Themes & Fluent Design

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages.

A theme is the complete visual definition of the standard controls: the colours, corner radii, spacing, animations and templates that decide what a Button, a TextBox or a ListBox looks like before the app adds a single style of its own. For almost two decades a WPF app got whatever theme the framework shipped with. Since .NET 9 WPF also ships a second theme, based on the Fluent design principles of Windows 11, with integrated light and dark modes and support for the accent colour that the user picks in Windows Settings. This page explains how to turn it on, what the experimental label means in practice, how the accent colour and dark mode behave, which controls are covered in .NET 10, and how to combine the built-in theme with the app’s own resources.

The page was written against these versions, current in October 2026:

  • .NET 10 LTS, latest patch 10.0.13, with C# 14. .NET 11 is at release candidate 2 (11.0.0-rc.2) and general availability is expected at .NET Conf, 10 to 13 November 2026; the .NET 11 WPF release notes list bug fixes only at the time of writing, so nothing here is described as changing for it. .NET 8 and .NET 9 leave support on 2026-11-10; the Fluent theme exists on .NET 9 and later, not on .NET 8.

  • CommunityToolkit.Mvvm 8.4.2 for the view models of the example.

  • Windows App SDK 2.5.1 (stable channel) for the WinUI 3 comparison.

  • Visual Studio 2026 18.11.1.

  • Windows 10 and Windows 11. The Fluent theme is designed after the Windows 11 look; Microsoft Learn does not state a minimum Windows version for it.

The running scenario is Bookshelf Desktop. Its BooksViewModel, IBookRepository and IDialogService are the ones used on the other pages of this section; here a small settings view model lets the user choose between the system, light and dark appearance.

What Microsoft Learn Provides, and What It Does Not

The material on this topic is spread over three places:

  • the What’s new in WPF for .NET 9 page, which contains the only prose description of the Fluent theme, of the ThemeMode values, of the WPF0001 experimental diagnostic and of the accent colour keys;

  • the What’s new in WPF for .NET 10 page, which lists the controls that gained Fluent styles in that release;

  • the API reference page for the ThemeMode struct.

There is no Fluent theming deep dive in the official WPF documentation. There is no page that documents the contents of Fluent.xaml (the keys it defines or how to override them), no list of every control that is styled, and no statement about Mica or other backdrops for WPF windows. Where this page goes beyond the three sources, it says so and presents the code as a pattern, not as an official sample. The WPF Gallery app, which Microsoft publishes in the Microsoft Store, is the official way to preview the theme on real controls.

Two Themes: Aero2 and Fluent

Without any setting, a WPF app on .NET uses the Aero2 theme: the look WPF has had since .NET Framework 4.5, which resembles Windows 8. It has no dark mode, it ignores the Windows accent colour, and it looks out of place next to the Windows 11 shell. The Fluent theme is an opt-in alternative. The two coexist in the framework and the choice is made per application or per window.

Aspect Aero2 (default) Fluent (opt-in, .NET 9 and later)

How to select it

Nothing to do; ThemeMode is None

Set ThemeMode, or merge Fluent.xaml

Light and dark modes

Light only

Both, integrated; System follows the Windows setting

Windows accent colour

Not used by the controls

Used by the controls

Where it lives

Part of the framework; not described further in the documentation used here

A separate assembly, PresentationFramework.Fluent, referenced by the pack URI of Fluent.xaml

Status

Stable

In progress: .NET 10 added more controls and fixes, and the code API for ThemeMode is experimental

Control coverage

All standard controls

Growing; see the table later on this page

The accent colour is not tied to the Fluent theme: the .NET 9 page states explicitly that the accent colour resources are available with or without it. The controls only use that colour automatically when the Fluent theme is on.

The Theme Resolution Flow

A control gets its visual definition by looking for its default style. With ThemeMode set, WPF selects the Fluent dictionary (light or dark) instead of the Aero2 theme dictionary, and the app’s own resources then sit on top of it as they always did.

flowchart TD A["Control is created"] --> B{"ThemeMode on the Window?"} B -->|"Set"| C["Window value"] B -->|"Not set"| D{"ThemeMode on the Application?"} D -->|"Set"| E["Application value"] D -->|"None or not set"| F["Aero2 theme"] C --> G{"Light, Dark or System?"} E --> G G -->|"System"| H["Read Windows app mode"] H --> I["Fluent light or Fluent dark"] G -->|"Light"| J["Fluent light"] G -->|"Dark"| K["Fluent dark"] I --> L["App and window resources override theme values"] J --> L K --> L F --> L

Microsoft Learn states the two rules this diagram relies on: ThemeMode is set per application with Application.ThemeMode and per window with Window.ThemeMode, and once the application value is anything other than None, a window can no longer be set back to None. The precise internal lookup order is not documented, so the diagram shows the documented behaviour, not the implementation.

ThemeMode

ThemeMode is a readonly struct in the System.Windows namespace, in PresentationFramework.dll, applicable to .NET 9, 10 and 11. It is decorated with [Experimental("WPF0001")]. Because it is experimental, the API reference asks you not to create instances yourself with the constructor and to use the four static properties:

Value Meaning

ThemeMode.Light

Applies the light Fluent theme.

ThemeMode.Dark

Applies the dark Fluent theme.

ThemeMode.System

Applies the light or the dark Fluent theme according to the user’s current Windows setting.

ThemeMode.None

The default. Uses the Aero2 theme.

In XAML the property is set as an attribute with these names. To style the entire application from the Windows setting, set it on the Application element of App.xaml:

<Application x:Class="Bookshelf.Desktop.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             ThemeMode="System">
    <Application.Resources>
        <!-- App-wide brushes, converters and styles go here as usual. -->
    </Application.Resources>
</Application>

A single window can force a mode regardless of the Windows setting. The Bookshelf "reading lamp" window is always dark, so that long reading sessions are easy on the eyes:

<Window x:Class="Bookshelf.Desktop.Views.ReaderWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Reader" Height="600" Width="900"
        ThemeMode="Dark">
    <Grid Margin="16">
        <TextBlock Text="{Binding CurrentChapter}" TextWrapping="Wrap" FontSize="18" />
    </Grid>
</Window>

ThemeMode is described as designed to respect the settings of a Fluent dictionary, so that the Fluent theme can be customised: values that the app defines in a dictionary are not discarded when the mode is applied. The details of how customisation and the mode interact are not documented beyond that sentence.

The Experimental Status and WPF0001

The attribute [Experimental("WPF0001")] is the standard .NET mechanism for marking an API that can change or be removed in a later release. A compiler that sees code using such an API reports the diagnostic ID as an error, not a warning, until the project opts in. The .NET 9 page applies this to the code side of ThemeMode: accessing the property from code produces error WPF0001, which prevents access to the API until the error is suppressed. The page itself marks the API as experimental and subject to change.

Two suppression options are documented. The project-wide one adds the ID to NoWarn in the project file:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net10.0-windows</TargetFramework>
    <UseWPF>true</UseWPF>
    <Nullable>enable</Nullable>
    <!-- Allow the experimental ThemeMode API in code. -->
    <NoWarn>$(NoWarn);WPF0001</NoWarn>
  </PropertyGroup>

</Project>

The narrower option is a #pragma around the lines that need the API, which keeps the rest of the project under the diagnostic. The .NET 9 page names this directive as an alternative to the project-wide setting:

using System.Windows;

namespace Bookshelf.Desktop.Theming;

public static class ThemeInterop
{
    // Confines the experimental API to one file, so a later change of ThemeMode
    // only needs to be fixed here.
#pragma warning disable WPF0001
    public static void ApplyToApplication(ThemeMode mode) => Application.Current.ThemeMode = mode;

    public static void ApplyToWindow(Window window, ThemeMode mode) => window.ThemeMode = mode;

    public static ThemeMode Light => ThemeMode.Light;

    public static ThemeMode Dark => ThemeMode.Dark;

    public static ThemeMode System => ThemeMode.System;
#pragma warning restore WPF0001
}

The static wrapper is a design choice, not part of the official sample: it gives the rest of the app a single place to adjust if the API changes shape before it is stabilised. The official examples assign the property directly:

// Set light mode at the application level
Application.Current.ThemeMode = ThemeMode.Light;

// Set dark mode on the current window (inside a Window's code-behind)
this.ThemeMode = ThemeMode.Dark;

The same page puts WPF0001 in the context of the code setter. It does not state whether the XAML attribute ThemeMode="System" also needs the suppression when the project is built, and it does not state when the API is expected to become stable. The pages of this section therefore treat ThemeMode as a feature to adopt deliberately: it is usable in production by apps that accept a possible source-level change on a later .NET version, and the resource dictionary approach below avoids the experimental API altogether.

The Fluent.xaml Resource Dictionary

The second way to apply the theme is to reference the dictionary directly. It is available at the pack URI /PresentationFramework.Fluent;component/Themes/Fluent.xaml, and the documented application-level usage merges it into the application resources with the absolute form of the pack URI:

<Application x:Class="Bookshelf.Desktop.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="pack://application:,,,/PresentationFramework.Fluent;component/Themes/Fluent.xaml" />
                <ResourceDictionary Source="Themes/Bookshelf.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

The same page says the dictionary can also be applied to a single Window, to theme just that window. The mechanism is the one explained on the Resources & Resource Dictionaries page of this section: a ResourceDictionary in Window.Resources is searched before the application resources, so the Fluent styles apply only to that window.

<Window x:Class="Bookshelf.Desktop.Views.AboutWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="About" Height="240" Width="420">
    <Window.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="pack://application:,,,/PresentationFramework.Fluent;component/Themes/Fluent.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Window.Resources>
    <StackPanel Margin="16">
        <TextBlock Text="Bookshelf Desktop" FontSize="20" />
        <Button Content="Close" Margin="0,16,0,0" IsCancel="True" />
    </StackPanel>
</Window>

The two approaches compare as follows:

Question ThemeMode Merged Fluent.xaml

Experimental diagnostic

Setting it in code needs WPF0001 suppressed; the XAML attribute is the documented no-code form

Not involved; it is an ordinary resource dictionary

Light and dark choice

Built in (Light, Dark, System)

The documentation does not describe a variant selection for the dictionary reference

Runtime change

Assign a new value

Replace or remove the merged dictionary

Scope

Application or one Window

Application resources, window resources, or any element that has resources

Documented customisation

"Respects the settings set by a Fluent Dictionary"

Add your own dictionaries after it so that your keys win

The Fluent.xaml file itself is not documented on Microsoft Learn: the list of keys is not published, so overriding a brush or a style key of the Fluent theme means reading the file in the dotnet/wpf repository for the version in use. That is a maintenance cost to weigh before overriding internals; prefer overriding at the level of the app’s own styles (see the custom themes section).

Accent Colours

Windows lets the user pick an accent colour. Since .NET 9, WPF exposes it through the SystemColors class, as a Color, as a SolidColorBrush and as the matching ResourceKey values. Along with the accent itself, three lighter and three darker shades are available:

Shade Color Color key Brush Brush key

Accent

AccentColor

AccentColorKey

AccentColorBrush

AccentColorBrushKey

Light 1

AccentColorLight1

AccentColorLight1Key

AccentColorLight1Brush

AccentColorLight1BrushKey

Light 2

AccentColorLight2

AccentColorLight2Key

AccentColorLight2Brush

AccentColorLight2BrushKey

Light 3

AccentColorLight3

AccentColorLight3Key

AccentColorLight3Brush

AccentColorLight3BrushKey

Dark 1

AccentColorDark1

AccentColorDark1Key

AccentColorDark1Brush

AccentColorDark1BrushKey

Dark 2

AccentColorDark2

AccentColorDark2Key

AccentColorDark2Brush

AccentColorDark2BrushKey

Dark 3

AccentColorDark3

AccentColorDark3Key

AccentColorDark3Brush

AccentColorDark3BrushKey

The documented guidance is to wrap the resource key in a dynamic resource, so that the colour in the open app updates if the user changes the accent while the app is running. The markup extensions are written with passthrough in inline code: {DynamicResource {x:Static SystemColors.AccentColorBrushKey}}. A Bookshelf "unread" badge and a section title use it like this:

<StackPanel Orientation="Horizontal" Margin="0,0,0,12">
    <Border Width="10" Height="10" CornerRadius="5" VerticalAlignment="Center" Margin="0,0,8,0"
            Background="{DynamicResource {x:Static SystemColors.AccentColorBrushKey}}" />
    <TextBlock Text="Unread" FontWeight="SemiBold"
               Foreground="{DynamicResource {x:Static SystemColors.AccentColorBrushKey}}" />
</StackPanel>

<!-- A darker shade for text on a light background: more contrast than the plain accent. -->
<TextBlock Text="Currently reading"
           Foreground="{DynamicResource {x:Static SystemColors.AccentColorDark1BrushKey}}" />

The AccentColorBrushKey resource key is looked up through the resource system, so the same DynamicResource rules as on the Resources & Resource Dictionaries page apply: a StaticResource captures the colour once and never updates. Reading the colour from code uses the properties, which return the current value:

using System.Windows;
using System.Windows.Media;

namespace Bookshelf.Desktop.Theming;

public static class Accent
{
    // The current Windows accent colour and a darker shade of it.
    public static Color Primary => SystemColors.AccentColor;

    public static Color Strong => SystemColors.AccentColorDark1;

    // Brushes are what most visual properties need.
    public static SolidColorBrush PrimaryBrush => SystemColors.AccentColorBrush;
}

A value read once in code is a snapshot. The documentation does not describe an event that tells code the accent changed; the dynamic resource is the supported way to follow it. This page does not claim that the property values change in a running app; use the DynamicResource form for anything that must follow the user’s choice.

Dark Mode

In the Fluent theme, dark mode is not a separate set of styles that the app writes: the controls have a light variant and a dark variant, and ThemeMode selects between them. There are three ways to arrive at dark:

  1. ThemeMode="Dark", which is dark regardless of Windows;

  2. ThemeMode="System" with Windows set to the dark app mode;

  3. a hand-written dark dictionary, as on the Resources & Resource Dictionaries page, for apps that stay on Aero2.

For a Bookshelf settings screen, the user picks among system, light and dark, and the view model remembers the choice. The view model uses [ObservableProperty] on a partial property (CommunityToolkit.Mvvm 8.4.2 and C# 14):

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

namespace Bookshelf.Desktop.ViewModels;

public enum ThemePreference
{
    System,
    Light,
    Dark,
}

public interface IFluentThemeService
{
    ThemePreference Current { get; }

    void Apply(ThemePreference preference);
}

public sealed partial class SettingsViewModel : ObservableObject
{
    private readonly IFluentThemeService _themes;

    public SettingsViewModel(IFluentThemeService themes)
    {
        _themes = themes;
        Preference = themes.Current;
    }

    [ObservableProperty]
    public partial ThemePreference Preference { get; set; }

    public IReadOnlyList<ThemePreference> Choices { get; } = Enum.GetValues<ThemePreference>();

    partial void OnPreferenceChanged(ThemePreference value) => _themes.Apply(value);

    [RelayCommand]
    private void Reset() => Preference = ThemePreference.System;
}

The service that maps the preference to ThemeMode is the only class that touches the experimental API. Registering it in the Generic Host is covered on the Dependency Injection & the Generic Host page of this section (not linked here because that page is not written yet); the registration is services.AddSingleton<IFluentThemeService, FluentThemeService>().

using System.Windows;
using Bookshelf.Desktop.ViewModels;

namespace Bookshelf.Desktop.Theming;

public sealed class FluentThemeService : IFluentThemeService
{
    public ThemePreference Current { get; private set; } = ThemePreference.System;

    public void Apply(ThemePreference preference)
    {
        Current = preference;

#pragma warning disable WPF0001   // ThemeMode is experimental in .NET 9 and 10
        Application.Current.ThemeMode = preference switch
        {
            ThemePreference.Light => ThemeMode.Light,
            ThemePreference.Dark => ThemeMode.Dark,
            _ => ThemeMode.System,
        };
#pragma warning restore WPF0001
    }
}

The view is an ordinary bound ComboBox:

<StackPanel Margin="24" MaxWidth="360" HorizontalAlignment="Left">
    <TextBlock Text="Appearance" FontSize="20" Margin="0,0,0,12" />
    <ComboBox ItemsSource="{Binding Choices}"
              SelectedItem="{Binding Preference, Mode=TwoWay}"
              AutomationProperties.Name="Theme" />
    <Button Content="Follow Windows" Margin="0,12,0,0" Command="{Binding ResetCommand}" />
</StackPanel>

The persistence of the choice (the next launch should start with it) is covered by a later page of the section on local settings; the view model above deliberately does not store it.

Windows chrome and the title bar

The ThemeMode documentation does not say whether the non-client area of the window (the title bar and its caption buttons) follows the mode. This page therefore makes no claim about it. Test the target Windows versions on a real machine, and if the title bar must match the content, check the WPF Gallery and the dotnet/wpf repository for the current behaviour. The same caution applies to window backdrops: the WPF documentation does not mention Mica for WPF windows, while WinUI 3 supports Mica and Acrylic as part of its design system (see the comparison table below).

High Contrast

Windows has a high contrast mode for accessibility, which replaces the app palette with a small set of system colours chosen by the user. Two statements from the documentation matter:

  • The .NET 10 notes list a fix for crashes related to HighContrast among the Fluent style fixes, so older .NET 9 builds are the more fragile place to rely on the Fluent theme in high contrast. The page does not describe the conditions of the crash.

  • The mechanism for apps remains the one from the Resources & Resource Dictionaries page: SystemParameters.HighContrast reports the mode, and SystemColors brushes give the colours the user chose.

A custom control that draws with its own brushes must stay readable in high contrast. The safe approach is to use the SystemColors keys for text and borders when the mode is on:

using System.Windows;

namespace Bookshelf.Desktop.Theming;

public sealed class HighContrastMonitor
{
    public HighContrastMonitor()
    {
        // SystemParameters.StaticPropertyChanged is raised when a system setting changes.
        SystemParameters.StaticPropertyChanged += (_, e) =>
        {
            if (e.PropertyName == nameof(SystemParameters.HighContrast))
            {
                HighContrastChanged?.Invoke(this, EventArgs.Empty);
            }
        };
    }

    public event EventHandler? HighContrastChanged;

    public bool IsHighContrast => SystemParameters.HighContrast;
}

The accessibility checklist for the whole app is on the globalization and accessibility page later in the section.

Which Controls the Fluent Theme Covers

The documentation does not publish a complete coverage list. What it states, from the .NET 10 notes, is a delta: "Fluent UI style support is still in progress", and .NET 10 added Fluent styles for these controls:

Control Release that added the Fluent style

DatePicker

.NET 10

GridSplitter

.NET 10

GridView (the ListView view)

.NET 10

GroupBox

.NET 10

Hyperlink

.NET 10

Label

.NET 10

NavigationWindow

.NET 10

RichTextBox

.NET 10

TextBox

.NET 10

The same section lists these non-control changes: the Expander animation was fixed, the missing RecognizesAccessKey was added to several controls, and right-to-left layout problems were fixed for MenuItem, Expander and TreeViewItem. A control not in the table may still be styled: it may have been styled in .NET 9 (which the .NET 9 page does not enumerate), or it may not be styled at all. Treat the table as the list of controls whose style is known to be new in .NET 10, and verify others visually. Third-party and custom controls are not themed unless they use the dictionary keys or default styles that the theme provides.

Two rules of thumb follow from the incomplete list:

  1. Test every window of the app on both modes (light and dark) with the controls that are really used. The WPF Gallery app shows how each standard control looks.

  2. When a control looks wrong, check for an app-level implicit Style that replaces the template. A style that sets Template replaces the Fluent template for that control, which is what the Dependency Properties & Attached Properties page means by "the app-level style overrides the framework look".

Custom Themes on Top of Fluent

An app brand colour or a custom control must coexist with the built-in theme. The pattern that works with both ThemeMode and the merged dictionary is to keep the app’s own tokens in a dictionary whose entries are not the same keys as the framework’s, reference them with DynamicResource, and let ThemeMode handle the standard controls. Brand brushes are defined in light and dark variants, as in the Resources & Resource Dictionaries page, and selected by a small theme service; the accent colour comes from SystemColors.

<!-- Themes/Bookshelf.xaml : tokens that do not collide with framework keys -->
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <!-- Spacing tokens are independent of the theme. -->
    <Thickness x:Key="Bookshelf.Spacing.Page">24</Thickness>
    <CornerRadius x:Key="Bookshelf.Radius.Card">8</CornerRadius>

    <!-- A card style that follows the accent colour and the theme-driven foreground. -->
    <Style x:Key="Bookshelf.Style.Card" TargetType="Border">
        <Setter Property="Padding" Value="16" />
        <Setter Property="CornerRadius" Value="{StaticResource Bookshelf.Radius.Card}" />
        <Setter Property="BorderThickness" Value="1" />
        <Setter Property="BorderBrush" Value="{DynamicResource {x:Static SystemColors.AccentColorBrushKey}}" />
    </Style>

    <!-- A named style for the page title; it does not replace the control template. -->
    <Style x:Key="Bookshelf.Style.PageTitle" TargetType="TextBlock">
        <Setter Property="FontSize" Value="24" />
        <Setter Property="FontWeight" Value="SemiBold" />
        <Setter Property="Margin" Value="0,0,0,12" />
    </Style>
</ResourceDictionary>

Used in the Bookshelf list screen:

<Border Style="{StaticResource Bookshelf.Style.Card}" Margin="{StaticResource Bookshelf.Spacing.Page}">
    <StackPanel>
        <TextBlock Style="{StaticResource Bookshelf.Style.PageTitle}" Text="My shelf" />
        <ListBox ItemsSource="{Binding Books}" SelectedItem="{Binding SelectedBook}" DisplayMemberPath="Title" />
    </StackPanel>
</Border>

Three guidelines keep a custom theme maintainable:

  • Style by type with BasedOn, not by replacing templates. A Style with TargetType="Button" and BasedOn="{StaticResource {x:Type Button}}" adds to the active theme instead of replacing it, so the control still gets the Fluent template. A style with a new ControlTemplate stops the control from following the theme.

  • Order of the merged dictionaries. Resources are searched in the order of the merged dictionaries from the last to the first, so the dictionary placed after Fluent.xaml wins. Put the app dictionary after the framework one.

  • Keep theme-dependent values behind DynamicResource. Anything that has to change when the user switches mode or accent colour is a dynamic reference. Spacing, radii and font sizes can stay StaticResource because they do not change.

A BasedOn example, so that a Bookshelf primary button inherits the Fluent look and only adds its own spacing:

<Style x:Key="Bookshelf.Style.PrimaryButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
    <Setter Property="Padding" Value="16,6" />
    <Setter Property="MinWidth" Value="96" />
</Style>

Whether {StaticResource {x:Type Button}} resolves to the Fluent default style when ThemeMode is used, rather than to the Aero2 one, is not stated in the sources used here. Verify with the WPF Gallery pattern on the target .NET version (look at a button next to one without BasedOn); if the BasedOn reference fails to pick up the theme in a given release, drop BasedOn and style only the properties you set.

Comparison With Other Stacks

Topic WPF (.NET 9 and later) WinUI 3 Windows Forms

Default look

Aero2; Fluent opt-in

Fluent (WinUI controls)

Classic Windows look

Selecting light or dark

ThemeMode on Application or Window (code API experimental)

Application.RequestedTheme (set at startup) or FrameworkElement.RequestedTheme per element; see the WinUI 3 page of this section

Application.SetColorMode: experimental (WFO5001) in .NET 9, stable in .NET 10 (no NoWarn needed)

Follow the Windows setting

ThemeMode.System

Follows the Windows app mode by default when RequestedTheme is not set

Application.SetColorMode(SystemColorMode.System); the other values are Classic (the light default) and Dark. The mode is read once when it is set, so a later Windows change is not followed

Accent colour

SystemColors.AccentColor* since .NET 9

SystemAccentColor and its Light1-Light3 and Dark1-Dark3 theme resources

Not provided by the controls

Backdrop materials (Mica, Acrylic)

Not documented for WPF

Supported

Not provided

Status of the feature

Experimental in code; control coverage growing

Stable

Stable in .NET 10

For the choice among the stacks, see Windows App Platforms: WPF, WinUI 3, WinForms, MAUI & More; for how a WinUI XAML file differs from a WPF one, see XAML Dialects: WPF, WinUI & MAUI.

Testing a Themed App

Because the same window can be shown in several modes, the cheap way to catch regressions is to open it in each:

  1. Run the app with ThemeMode="Light", then "Dark", then switch Windows to the other app mode while System is active and see whether the window follows.

  2. Change the Windows accent colour while the app is open and confirm the accent-coloured elements update.

  3. Turn high contrast on and confirm the text is readable and focus visuals remain visible.

  4. Review any window that has a custom Style that sets Template or colours directly, because those do not follow the theme.

  5. Use the tools described in Development Environment & Tooling to inspect the running visual tree.

A view-model unit test cannot see the theme, but it can verify that the settings view model asks the service for what the user chose. That test uses a fake IFluentThemeService, which keeps the experimental API out of the test project:

using Bookshelf.Desktop.ViewModels;
using Xunit;

namespace Bookshelf.Desktop.Tests;

public sealed class SettingsViewModelTests
{
    private sealed class FakeThemes : IFluentThemeService
    {
        public ThemePreference Current { get; private set; } = ThemePreference.System;

        public ThemePreference? LastApplied { get; private set; }

        public void Apply(ThemePreference preference)
        {
            Current = preference;
            LastApplied = preference;
        }
    }

    [Fact]
    public void Choosing_dark_applies_dark()
    {
        var themes = new FakeThemes();
        var vm = new SettingsViewModel(themes);

        vm.Preference = ThemePreference.Dark;

        Assert.Equal(ThemePreference.Dark, themes.LastApplied);
    }

    [Fact]
    public void Reset_returns_to_system()
    {
        var themes = new FakeThemes();
        var vm = new SettingsViewModel(themes) { Preference = ThemePreference.Light };

        vm.ResetCommand.Execute(null);

        Assert.Equal(ThemePreference.System, vm.Preference);
    }
}

Pitfalls

  • Experimental means experimental. The code API is subject to change; keep it in one class such as the FluentThemeService above, and re-read the What’s new in WPF page of each .NET version before upgrading.

  • WPF0001 is an error, not a warning. Without NoWarn or the #pragma, a project that sets ThemeMode in code does not compile. Do not add a blanket NoWarn that hides unrelated diagnostics; add WPF0001 only.

  • No way back to None per window. If Application.ThemeMode is not None, a window cannot be set to None. A window that must stay on Aero2 requires the application to be on None and the other windows to opt in.

  • Static references to theme brushes. StaticResource on an accent or a theme brush keeps the old colour after the user changes mode or accent. Use DynamicResource.

  • Replacing templates. An app-wide implicit style that sets Template for a control removes the Fluent template from it; check the control in both modes.

  • Assuming complete coverage. .NET 10 still describes Fluent support as in progress. A control that is not styled looks like Aero2 inside a Fluent window. Test every control the app uses.

  • Overriding framework keys. The keys inside Fluent.xaml are not documented as a stable contract; an override can stop working on a later .NET version.

  • Third-party and custom controls. Controls that ship their own templates or colours do not follow ThemeMode. Check their documentation for Fluent or dark-mode support.

  • High contrast on .NET 9. The .NET 10 notes mention fixed crashes related to HighContrast in the Fluent styles; test high contrast on the exact .NET version you ship.

  • Third-party claims beyond Microsoft Learn. Articles that describe Mica, title bar and accent-colour behaviour for WPF go beyond what Microsoft Learn documents. Treat them as hints, and test.