Dark Mode and Contrast Themes in Windows Apps — DWM Dark Title Bars, System Theme Tracking in WinForms/WPF, and Drawing under High Contrast

· Updated: · · Dark Mode, Contrast Themes, High Contrast, DWM, WinForms, WPF, Windows 11, Accessibility, Business Applications, Windows

Revision history (first version, published Sep 2, 2026)
First published
Cite this article(DOI: 10.5281/zenodo.22640278)

This article is archived on Zenodo. Below are both the DOI that always resolves to the latest version and the DOI pinned to the version you are reading.

Go Komura (2026). Dark Mode and Contrast Themes in Windows Apps — DWM Dark Title Bars, System Theme Tracking in WinForms/WPF, and Drawing under High Contrast. KomuraSoft LLC. https://doi.org/10.5281/zenodo.22640278 https://comcomponent.com/en/blog/windows-app-dark-mode-contrast-theme-guide/

DOI (latest version)
10.5281/zenodo.22640278
DOI (this version)
10.5281/zenodo.22640279

“We replaced the office PCs with Windows 11, and the employees who use dark mode complain that our business app is the only one whose title bar is blinding white.” “An employee with low vision turned on a contrast theme, and the status display on the order screen vanished.” Both are complaints we have heard more often over the past year or two.

The first comes from the Colors setting in Windows 11, the second from the Accessibility settings, but to a developer they look like the same problem: the app does not follow the theme. And in fact the foundation of the fix is shared. Do not hardcode colors; read the system setting, notice changes, and repaint. Those three points.

The previous article, “An Introduction to Windows App Accessibility”, covered how screen readers read an app (UI Automation) and the basics of naming, keyboard operation, and color. It touched on following contrast themes, but it did not cover the light/dark color mode itself. This article is its companion piece. It connects the two axes of color mode (light/dark) and contrast themes at the implementation level, in this order: title-bar drawing by DWM (the Desktop Window Manager), following the system theme in WinForms/WPF, and drawing under a contrast theme. Intended readers are developers who build and maintain business apps in WinForms, WPF, or Win32. Prerequisites are Windows 11 (dark title bars require build 22000 or later) and WinForms/WPF on .NET 9/10 (on .NET Framework 4.8 and .NET 8, some parts must be implemented by hand). The difficulty is intermediate.

The flow of this articleThe structure of this article, which connects in order the two theme axes, why windows default to light, DWM dark title bars, the detection and tracking mechanism, the WinForms and WPF implementations, drawing under a contrast theme, deciding a policy, and verificationSort out the two theme axesWhy the default is lightDWM dark title barDetection and trackingWinForms/WPF implementationDrawing under a contrast themeDeciding a policy and verifying

Figure 1: This article runs in one line from sorting out the themes through mechanisms, implementation, contrast themes, and verification.

1. The Bottom Line First

  • Windows has two theme axes. Light/dark (the color mode) under Settings > Personalization > Colors, and Settings > Accessibility > Contrast themes. The latter is a palette constrained to a contrast ratio of roughly 7:1 or higher, and it is a different thing from light/dark. Dark mode is unavailable while a contrast theme is active. The order of precedence for detection is “contrast theme, then light/dark”.12
  • An existing app’s title bar stays white because that is the compatibility default. Windows has no way of knowing whether an app supports dark mode, so it treats every window as light by default.3
  • What makes the title bar dark is DWMWA_USE_IMMERSIVE_DARK_MODE (value 20) via DwmSetWindowAttribute. Pass a BOOL TRUE and the frame is drawn dark when the system is dark. The documented support is Windows 11 build 22000 or later.43
  • Read the current mode with UISettings.GetColorValue and receive changes through ColorValuesChanged. Microsoft’s official procedure is: if the foreground (the default text color) is bright, the mode is dark. The event is not guaranteed to arrive on the UI thread, so marshal back to the UI before repainting.35
  • WinForms gained Application.SetColorMode in .NET 9, and it stopped being experimental in .NET 10. Call it with SystemColorMode.System before Application.Run. It has three constraints: Windows 11 only, disabled while a contrast theme is active, and no tracking of setting changes while the app is running.672
  • WPF gained the Fluent theme and ThemeMode in .NET 9. ThemeMode="System" follows the system and also controls darkening the window. However, manipulating it from code is still experimental in .NET 10 (WPF0001), and the Fluent styles are “in progress”. If you stay on the classic theme, swap light and dark ResourceDictionaries referenced through DynamicResource.8910
  • Under a contrast theme, map colors to the correct system-color pairs, drop images behind text, and draw multicolor graphics in the two foreground and background colors. Detect with SPI_GETHIGHCONTRAST (SystemInformation.HighContrast in WinForms, SystemParameters.HighContrast in WPF); notifications are WM_SYSCOLORCHANGE / WM_THEMECHANGED (SystemEvents.UserPreferenceChanged in .NET).111213
  • Dark mode support is not a substitute for accessibility support. A dark palette still needs a 4.5:1 contrast ratio, and conveying information by more than color alone is required under every theme.1415

In one sentence: theme support means gathering colors in one place, reading the system setting, noticing changes, and repainting; but under a contrast theme, deferring entirely to the system-color pairs.

In the diagram a solid line marks a relation that always holds and a dashed line marks a conditional one (the conditions are given per relation on the detail page). The full list of relations (28 in total, with evidence and certainty) and the definitions of the main concepts are collected on the knowledge map detail page (in Japanese). Data: JSON-LD / Turtle

2. “Theme” Has Two Axes — Light/Dark and Contrast Themes

2.1. Light/Dark (Color Mode)

The color mode under Settings > Personalization > Colors in Windows is the setting that decides the brightness of foreground and background across the OS and all apps. Microsoft’s documentation defines light as “a dark foreground on a light background” and dark as “a light foreground on a dark background”, adding that foreground here means “the default text color”. In dark mode the foreground (text) is light and the background is dark.3

The setting is stored in the registry under HKCU\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize as the DWORD values AppsUseLightTheme (the app mode) and SystemUsesLightTheme (the mode of Windows itself), and it is listed in Microsoft’s settings reference.16 As described below, however, the canonical way to read it from an app is the WinRT UISettings class.

2.2. Contrast Themes (High Contrast)

A contrast theme, chosen under Settings > Accessibility > Contrast themes, uses a palette constrained to a contrast ratio of roughly 7:1 or higher, and exists for users who need a strong visual separation of foreground and background. Windows 11 has four built in, Aquatic, Desert, Dusk, and Night sky, and the user can not only pick one of them but also edit the background, text, hyperlink, disabled text, selected text, and button colors individually. Left Alt + left Shift + Print Screen toggles a contrast theme quickly, and Aquatic is applied if none has been selected.1

Microsoft’s documentation states plainly: “do not confuse contrast themes with light and dark themes”. Light/dark uses a broad palette and is not optimized for maximum contrast.1 And the important part is that dark mode is unavailable while a contrast theme is active. WinForms’ Application.SetColorMode does not provide dark mode during a contrast theme, and XAML’s RequestedTheme is overridden by the system.217

2.3. The Order of Precedence

The implementation in an app therefore follows this order. First decide whether a contrast theme is active; if it is, defer entirely to the system colors. If it is not, choose either the light or the dark palette.

The two theme axes and the order of precedenceThe decision order in which the app defers entirely to the system-color pairs if a contrast theme is active, and otherwise reads the light/dark color mode and picks the app paletteYesNoLightDarkContrast theme active?Defer to the system-color pairsColor mode?Light paletteDark paletteDark mode is unavailable

Figure 2: Put the contrast-theme decision first, and pick the light or dark palette only when no contrast theme is active.

3. Why Existing Apps Stay White in Dark Mode

A window consists of two areas: the non-client area, made up of the title bar, the frame, and the caption buttons, and the client area, which the app draws. Since Windows Vista, the non-client area has been composed and drawn by DWM (the Desktop Window Manager), and the app specifies attributes of how it is drawn through DwmSetWindowAttribute.18

Microsoft’s documentation explains frankly why existing apps stay white. “Windows doesn’t know whether an application can support dark mode, so it assumes that it can’t for backward compatibility reasons.” Frameworks such as WinUI and the Windows App SDK handle dark mode natively, but Win32 apps usually do not support dark mode, so Windows gives them a light title bar by default.3

The two areas of a window and who draws themThe non-client area made up of the title bar and frame is drawn by DWM, and the client area is drawn by the app or UI framework, so dark mode support is needed in bothTop-level windowNon-client area (title bar, frame)Client area (the window contents)Composed and drawn by DWMDrawn by the app or frameworkInstructed via DwmSetWindowAttributeThe app's own palette

Figure 3: DWM draws the title bar and the app draws the contents, so dark mode support needs both an instruction to DWM and the app’s palette.

Two consequences follow. First, to make the title bar dark, the app must explicitly ask DWM. Second, what becomes dark as a result of asking is only the title bar; the client area must be repainted by the app itself. The documentation also says that “to fully support dark mode, the entire surface of the app needs to follow the dark theme”, and notes that the official guide covers only detection and the title bar, not how to repaint the client area.3 An app with a black title bar and white contents looks less natural than one that stays white throughout.

How the default becomes lightWindows does not know whether an app supports dark mode, so for compatibility it defaults to light, and only when the app passes TRUE through the DWM attribute does it draw the frame according to the system's dark settingNoYesWindows cannot tell whether the app supports itDefault is light, for compatibilityDid the app pass TRUE?Always a light frameDrawn according to the system setting

Figure 4: Not knowing whether the app supports it, Windows defaults to light and follows the system only when the app explicitly says so.

4. The DWM Dark Title Bar — DwmSetWindowAttribute

4.1. DWMWA_USE_IMMERSIVE_DARK_MODE

The attribute that makes the title bar dark is DWMWA_USE_IMMERSIVE_DARK_MODE. The DWMWINDOWATTRIBUTE enumeration describes it like this: “Allows the window frame for this window to be drawn in dark mode colors when the dark mode system setting is enabled. For compatibility reasons, all windows default to light mode regardless of the system setting. The pvAttribute parameter points to a value of type BOOL. TRUE to honor dark mode for the window, FALSE to always use light mode. This value is supported starting with Windows 11 Build 22000.”4

In other words, TRUE does not mean “make it dark”; it is permission that says “you may draw it dark if the system is dark”. If the app is prepared to paint its client area dark, passing TRUE is all it takes for the title bar to follow the system setting. Conversely, if the app is designed to always display in light mode (the “fixed light” policy described later), leaving the default FALSE is fine.

The C++ code in the official guide takes the following form. It even includes the step of defining value 20 yourself for older SDKs whose headers lack the constant.3

#include <dwmapi.h>
#pragma comment(lib, "dwmapi.lib")

#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
#endif

// Windows 11 (build 22000) or later? Assumes a manifest that declares supportedOS
// for Windows 10 or later (without one, the version is rounded down to Windows 8)
bool IsWindows11OrGreater()
{
    OSVERSIONINFOEXW osvi{ sizeof(osvi) };
    osvi.dwMajorVersion = 10;
    osvi.dwMinorVersion = 0;
    osvi.dwBuildNumber = 22000;
    DWORDLONG mask = 0;
    VER_SET_CONDITION(mask, VER_MAJORVERSION, VER_GREATER_EQUAL);
    VER_SET_CONDITION(mask, VER_MINORVERSION, VER_GREATER_EQUAL);
    VER_SET_CONDITION(mask, VER_BUILDNUMBER, VER_GREATER_EQUAL);
    return ::VerifyVersionInfoW(
        &osvi, VER_MAJORVERSION | VER_MINORVERSION | VER_BUILDNUMBER, mask) != FALSE;
}

// honorDarkMode = true: the title bar may be drawn dark when the system is dark
void ApplyTitleBarTheme(HWND hwnd, bool honorDarkMode)
{
    if (!IsWindows11OrGreater())
    {
        // Documented support is build 22000 or later. Below that, do not call it and follow the default (light)
        LogInfo(L"DWMWA_USE_IMMERSIVE_DARK_MODE is not documented for this OS build; keeping the default light frame");
        return;
    }
    BOOL value = honorDarkMode ? TRUE : FALSE;
    HRESULT hr = ::DwmSetWindowAttribute(
        hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, &value, sizeof(value));
    if (FAILED(hr))
    {
        // A failure on a supported OS is abnormal. Do not swallow it silently; record the HRESULT and surface it
        LogWarning(L"DwmSetWindowAttribute(DWMWA_USE_IMMERSIVE_DARK_MODE) failed: 0x%08X", hr);
    }
}

A word on why the call is gated on the OS version. The documented support is Windows 11 build 22000 or later.4 Reports that the same value works on Windows 10 are not rare, but a business app’s display should not depend on undocumented behavior. If the design is “try the call and give up on failure”, then when the call happens to succeed on Windows 10, the title bar turns dark on top of undocumented behavior. Below build 22000, do not call it and follow the documented default of a light title bar; on a supported OS, log the HRESULT of a failure and surface it. That is all. The internet also circulates an older procedure that uses value 19, and a technique that calls ordinal exports of uxtheme.dll to darken the common controls, but both are undocumented APIs, and nobody guarantees them when an update changes their behavior.

4.2. When to Call It — While the HWND Is Alive, and Every Time It Is Recreated

DwmSetWindowAttribute is called on an HWND, so it must run after the window handle has been created. And a WinForms form can have its handle recreated, for example when ShowInTaskbar changes. The new HWND after recreation carries no attribute, so the place to call it is not the constructor but the place that runs every time a handle is created: OnHandleCreated in WinForms, SourceInitialized in WPF.

When to call DwmSetWindowAttributeSet the DWM attribute after the window handle is created, set it again on the new handle when the handle is recreated, and set it again when a theme change notification arrivesHandle recreatedTheme change notificationHWND createdSet the DWM attributeDisplayed

Figure 5: The DWM attribute is tied to the HWND, so set it again on every creation and every recreation.

The P/Invoke in WinForms looks like this (for the safe way to write DllImport, see “Calling Win32 APIs Safely from C#”).

using System.Runtime.InteropServices;

public partial class MainForm : Form
{
    private const int DWMWA_USE_IMMERSIVE_DARK_MODE = 20;

    [DllImport("dwmapi.dll")]
    private static extern int DwmSetWindowAttribute(
        IntPtr hwnd, int attribute, ref int value, int size);

    protected override void OnHandleCreated(EventArgs e)
    {
        base.OnHandleCreated(e);
        ApplyTitleBarTheme();
    }

    private void ApplyTitleBarTheme()
    {
        // Documented support is build 22000 or later. Below that, do not call it and follow the default (light).
        // This check also works on .NET Framework. On .NET Framework, however, without a manifest that
        // declares supportedOS for Windows 10 or later, the version is rounded down to Windows 8
        // (on .NET 5 or later, OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000) works too)
        if (Environment.OSVersion.Version < new Version(10, 0, 22000))
        {
            _logger.LogInformation("Dark title bar is not documented for this OS build; keeping the default light frame");
            return;
        }
        // 1 (TRUE) = may be drawn dark when the system is dark. 0 (FALSE) = always light
        int honorDarkMode = 1;
        int hr = DwmSetWindowAttribute(
            Handle, DWMWA_USE_IMMERSIVE_DARK_MODE, ref honorDarkMode, sizeof(int));
        if (hr < 0)
        {
            _logger.LogWarning("DwmSetWindowAttribute failed: 0x{Hr:X8}", hr);
        }
    }
}

This code is needed by apps on .NET 8 or earlier, .NET Framework, and Win32/MFC. When you use Application.SetColorMode in WinForms on .NET 9 or later, or ThemeMode in WPF on .NET 9 or later, the framework takes over darkening the window (the ThemeMode documentation states that it “also controls the application of backdrop material and dark mode to the window”).10 Calling it twice does no harm, but it blurs who is responsible, so pick one or the other.

In WPF, the HWND is settled at SourceInitialized. Get the handle from WindowInteropHelper.19

using System.Windows.Interop;

public partial class MainWindow : Window
{
    protected override void OnSourceInitialized(EventArgs e)
    {
        base.OnSourceInitialized(e);
        var hwnd = new WindowInteropHelper(this).Handle;
        TitleBarTheme.Apply(hwnd, honorDarkMode: true); // the body is the P/Invoke shown above
    }
}

4.3. Title-Bar Color, Text Color, Border Color, and Backdrop Material

Windows 11 added attributes that specify the title-bar color itself, beyond the binary choice of dark or light.

Attribute Value Meaning Supported build
DWMWA_USE_IMMERSIVE_DARK_MODE 20 Draw the frame dark when the system is dark (BOOL) 22000
DWMWA_BORDER_COLOR 34 Window border color (COLORREF). DWMWA_COLOR_NONE removes the border 22000
DWMWA_CAPTION_COLOR 35 Title-bar color (COLORREF) 22000
DWMWA_TEXT_COLOR 36 Title text color (COLORREF) 22000
DWMWA_SYSTEMBACKDROP_TYPE 38 System-drawn backdrop material (Mica or Acrylic) 22621

For the three color attributes, passing DWMWA_COLOR_DEFAULT (0xFFFFFFFF) restores the system default. Note that for the border color, “it is the app’s responsibility to change the color in response to state changes such as window activation”.4 The backdrop material is specified with the DWM_SYSTEMBACKDROP_TYPE enumeration; on Windows 11, DWMSBT_MAINWINDOW corresponds to Mica and DWMSBT_TRANSIENTWINDOW to Acrylic, but the documentation states that “the effect of the material may change in future versions of Windows”.20

Think carefully about where this belongs in a business app. Once you paint the title bar in a brand color, you become responsible for guaranteeing the contrast of the title text and caption buttons on that color. On top of the two states dark and light, the combinations with active and inactive multiply. For most business apps the right answer is “follow the system default (just set value 20 to TRUE)”, and a brand color is an option for when it is genuinely needed.

How to decide the title-bar colorFollowing the system default only requires setting value 20 to TRUE, but painting a brand color makes the app responsible for guaranteeing the contrast of text and caption buttons and for managing active and inactive colors, and restoring passes DWMWA_COLOR_DEFAULTFollow the system defaultPaint a brand colorTitle-bar color?Just set value 20 to TRUESpecify the color attributes (34 to 36)Guarantee text and button contrast yourselfManage active/inactive yourselfRestore with COLOR_DEFAULT

Figure 6: Choosing a brand color moves the responsibility for contrast and state management to the app, so for most business apps following the default is the right answer.

5. Detecting and Following the System Theme — Read, Notice, Repaint

The job of making the client area follow the theme breaks down into three parts: read the current setting, notice changes, and repaint.

The three steps of detection and trackingThe loop of reading the current color mode with UISettings at startup, noticing changes through notifications such as ColorValuesChanged, marshaling back to the UI thread, and repainting the app paletteRead: UISettings.GetColorValueRepaint: reapply the paletteNotice: ColorValuesChangedMarshal to the UI threadSet the DWM attribute again too

Figure 7: Read at startup, notice through notifications, marshal to the UI thread, and repaint: that loop is the skeleton of theme tracking.

5.1. Read — UISettings and “If the Foreground Is Bright, It Is Dark”

Microsoft’s official procedure uses the WinRT class Windows.UI.ViewManagement.UISettings. Get the foreground color (the default text color) with GetColorValue(UIColorType::Foreground), estimate its perceived luminance with integer arithmetic to decide whether it is “bright”, and conclude dark mode if the foreground is bright. The documentation notes that the formula is not a rigorous luminance model, only an approximation sufficient for classifying light and dark.321

UISettings is a WinRT class, but C# WPF and WinForms apps can call it directly if the TargetFramework carries a Windows SDK version, such as net8.0-windows10.0.19041.0 (for how this works, see “WinRT Is COM”). You could also read AppsUseLightTheme from the registry directly, but the registry is where the setting is stored, not an API contract; if you read it, treat UISettings as the source of truth and keep the registry for diagnostics.

Ways to read the color modeThe canonical path is to get the foreground color through WinRT UISettings and classify it as light or dark; the registry value AppsUseLightTheme is the storage location and should be kept for diagnosticsLight or dark right now?UISettings.GetColorValueRegistry AppsUseLightThemeJudge the foreground's perceived luminanceBright means dark modeStorage location. Keep for diagnostics

Figure 8: The canonical read path is UISettings; the registry is only where the setting is stored.

5.2. Notice — ColorValuesChanged Does Not Arrive on the UI Thread

UISettings is also used to detect changes. The ColorValuesChanged event fires when a color value changes, and the official guide uses this event to track setting changes.53 One practical caution applies here. This event is not guaranteed to arrive on the UI thread. Marshal back to the UI thread with WPF’s Dispatcher, WinForms’ Control.Invoke, or the SynchronizationContext that works in both, before touching any control. Working with the UI thread is summarized in “The UI Thread and async/await in WPF/WinForms”.

The event also fires when the accent color changes. If you want to repaint only when light/dark changes, re-evaluate on every event and notify only when the result differs from last time. And, per the order of precedence in Chapter 2, do not look at the foreground brightness while a contrast theme is active. A contrast theme with a dark background such as Aquatic has a bright foreground, so brightness alone would misclassify it as “dark”. Treat the contrast theme as an independent state and decide it first.

The following class gathers that decision order into one place. The SynchronizationContext used to marshal back to the UI thread and the contrast-theme check (SystemInformation.HighContrast in WinForms, SystemParameters.HighContrast in WPF; see Chapter 8) are passed in by the caller. Mind when you create it. In WinForms, at the point of Program.Main there is no message loop and no Control yet, so SynchronizationContext.Current is null. Pass SynchronizationContext.Current after the controls exist, such as from the form’s constructor or OnLoad. In WPF you can pass new DispatcherSynchronizationContext(Application.Current.Dispatcher).

using Windows.UI.ViewManagement; // TargetFramework: net8.0-windows10.0.19041.0 or later

public enum ThemeState { Light, Dark, HighContrast }

public sealed class SystemThemeWatcher : IDisposable
{
    private readonly UISettings _settings = new();
    private readonly SynchronizationContext _ui;
    private readonly Func<bool> _isHighContrast;

    private bool _disposed;

    public ThemeState Current { get; private set; }
    public event EventHandler? Changed;

    // ui: the SynchronizationContext of the UI thread. Pass SynchronizationContext.Current
    //     after the controls exist, or a DispatcherSynchronizationContext in WPF
    // isHighContrast: () => SystemInformation.HighContrast (WinForms)
    //                 () => SystemParameters.HighContrast (WPF)
    public SystemThemeWatcher(SynchronizationContext ui, Func<bool> isHighContrast)
    {
        _ui = ui ?? throw new ArgumentNullException(nameof(ui));
        _isHighContrast = isHighContrast ?? throw new ArgumentNullException(nameof(isHighContrast));
        Current = Read();
        _settings.ColorValuesChanged += OnColorValuesChanged;
    }

    private ThemeState Read()
    {
        // Decision order as in Chapter 2: contrast theme first. A contrast theme with a dark
        // background has a bright foreground, so brightness alone would misclassify it as "dark"
        if (_isHighContrast()) return ThemeState.HighContrast;
        // Same test as the official guide: dark if the foreground (default text color) is bright
        var fg = _settings.GetColorValue(UIColorType.Foreground);
        bool isDark = (5 * fg.G + 2 * fg.R + fg.B) > 8 * 128;
        return isDark ? ThemeState.Dark : ThemeState.Light;
    }

    // Call from the UI thread. Can also be called from other notification paths such as UserPreferenceChanged
    public void Refresh()
    {
        if (_disposed) return;
        var next = Read();
        // Between light and dark, do not notify when the state is unchanged, to ignore accent-color-only changes.
        // During a contrast theme this is the exception: the state stays HighContrast even when the user edits
        // the theme's colors, so notify even for the same state so that the system colors are read again
        if (next == Current && next != ThemeState.HighContrast) return;
        Current = next;
        Changed?.Invoke(this, EventArgs.Empty);
    }

    private void OnColorValuesChanged(UISettings sender, object args)
    {
        // Not guaranteed to arrive on the UI thread, so marshal to the UI before deciding and notifying.
        // A call that arrives after Dispose (already queued) is ignored by the flag in Refresh
        _ui.Post(_ => Refresh(), null);
    }

    public void Dispose()
    {
        // Unsubscribing only stops future deliveries; calls already posted to the UI thread remain.
        // Set the flag so that the remaining calls ignore themselves (call this on the UI thread)
        _disposed = true;
        _settings.ColorValuesChanged -= OnColorValuesChanged;
    }
}

At the Win32 level, WM_SETTINGCHANGE is sent to every top-level window when a setting changes,22 and in .NET it arrives as SystemEvents.UserPreferenceChanged.23 A visual-style switch (including activating a contrast theme) brings WM_THEMECHANGED,24 and a system-color change brings WM_SYSCOLORCHANGE.25 Rather than writing separate handling for each kind of notification, call the same “read and repaint” routine whichever notification arrives; that is harder to break, and it has the same shape as the contrast-theme tracking introduced in the previous article. In terms of the SystemThemeWatcher above, it means calling Refresh() from the UserPreferenceChanged and StaticPropertyChanged handlers as well.

Theme change notification paths and threadsColorValuesChanged from UISettings may arrive off the UI thread and is marshaled back, WM_SETTINGCHANGE arrives as SystemEvents.UserPreferenceChanged, and WM_THEMECHANGED and WM_SYSCOLORCHANGE arrive at the window procedure. All of them converge on the same reapply routineColorValuesChangedMarshal to the UI threadUserPreferenceChangedRead and repaintWM_THEMECHANGED etc.

Figure 9: There are several notification paths, but all of them converge on the same “read and repaint” routine.

5.3. Repaint — Gather Colors in One Place

The precondition that makes repainting possible is that colors are gathered in one place. If Color.White and #FFFFFF are scattered across forms and XAML, you cannot enumerate the places to repaint. In WinForms, create a “palette” class (two instances, one light and one dark) and have controls take their colors from it at startup and on each notification. In WPF, gather colors in a ResourceDictionary and reference them from XAML with DynamicResource. In WinUI, this structure is provided from the start as ThemeDictionaries.

The structure that gathers colors in one placeThe app holds one light palette and one dark palette, and each screen takes its colors from the palette selected for the current mode, so the targets of a repaint can be enumeratedLightDarkCurrent modeWhich one?Light paletteDark paletteEach screen and controlScattered Color.WhiteCannot enumerate what to repaint

Figure 10: With the palette in one place a repaint can be enumerated; with scattered hardcoded colors it cannot.

This work of “gathering colors” pays off directly in contrast-theme support and in the contrast-ratio checks described later. The biggest cost of dark mode support is not the API calls but this cleanup.

6. Implementation in WinForms

6.1. .NET 9/10 — Application.SetColorMode

WinForms gained preliminary dark mode support in .NET 9, and it was “fully integrated” in .NET 10. Application.SetColorMode accepts three values.67

  • SystemColorMode.Classic — the default. Light, as before.
  • SystemColorMode.System — follow the light/dark setting of Windows.
  • SystemColorMode.Dark — dark.

Call it before Application.Run, before any UI element is created. On .NET 9 it was an experimental feature, so it was a compile error unless WFO5001 was suppressed in the project file; from .NET 10 the error no longer appears.26

static class Program
{
    [STAThread]
    static void Main()
    {
        ApplicationConfiguration.Initialize();
        Application.SetColorMode(SystemColorMode.System); // call before creating any UI
        Application.Run(new MainForm());
    }
}

When the color mode changes, System.Drawing.SystemColors switches to the matching colors, and the standard controls are drawn accordingly.6 Under the hood, the experimental property SystemColors.UseAlternativeColorSet (SYSLIB5002) “makes the system KnownColor values return an alternative color set (currently the dark mode version)”; because the Win32 system colors themselves do not change with the light/dark setting, .NET carries the alternative set on its side. The same documentation also says that when a contrast theme is active, the current Windows colors are always returned.27

Keep in mind the three constraints written in the SetColorMode documentation.2

  1. The dark color mode is available only on Windows 11 or later.
  2. Dark mode is unavailable when a contrast theme is active.
  3. Even with SystemColorMode.System, the app does not automatically follow a change to the Windows setting while it is running.

The third one tends to generate support questions in business apps. Write into the user-facing documentation that the mode is decided by the Windows setting at startup and takes effect at the next launch. If you absolutely must follow a switch at runtime, you need a design that recreates the forms, and it is almost never worth it.

The flow and constraints of SetColorModeSetColorMode is called before Application.Run, SystemColors switches to the alternative set, and the standard controls follow. It has three constraints: Windows 11 only, disabled during a contrast theme, and no tracking of setting changes at runtimeSetColorMode (System)Before Application.RunSystemColors switches to the alternative setStandard controls followWindows 11 onlyDisabled during a contrast themeDoes not follow changes at runtime

Figure 11: SetColorMode takes effect once before startup and has three documented constraints.

6.2. Owner-Drawn Controls and ApplyThemingImplicitly

The standard controls follow the app’s color mode, but the .NET 10 documentation lists two exceptional cases. If a control that you compose and draw yourself uses Win32 common controls such as scroll bars, those stay light unless they opt in explicitly. Conversely, if you inherit an existing control that follows the theme and want full control of its drawing yourself, you opt out.7

In both cases, override Control.CreateParams and call SetStyle(ControlStyles.ApplyThemingImplicitly, true/false) before reading base.CreateParams. The trap in this API is that the base-class constructor reads CreateParams, so your own constructor is too late.7

public partial class GanttChartControl : Control
{
    protected override CreateParams CreateParams
    {
        get
        {
            // Set it before reading base.CreateParams. The constructor is too late
            SetStyle(ControlStyles.ApplyThemingImplicitly, true);
            return base.CreateParams;
        }
    }
}
When ApplyThemingImplicitly can be setApplyThemingImplicitly is decided at the point where the base-class constructor reads CreateParams, so SetStyle must be called before base.CreateParams inside the CreateParams override, and the derived-class constructor is too lateBase-class constructorReads CreateParamsSetStyle must happen by hereDerived-class constructorCalling it here is too late

Figure 12: ApplyThemingImplicitly must be decided before the base-class constructor reads CreateParams.

Owner drawing itself (GDI+ drawing in OnPaint) follows the alternative set as long as it uses SystemColors / SystemBrushes / SystemPens. Any place that paints with Color.White is replaced with the palette described above, here too.

6.3. .NET Framework 4.8 and .NET 8 or Earlier — Declare “Fixed Light”

In environments without SetColorMode, there is no standard dark mode support. There are two options.

  1. Declare fixed light. Leave the DWM attribute at its default FALSE (always a light title bar) and keep the client area as it is. Contrast-theme support (Chapter 8) is still mandatory.
  2. Implement full support yourself. Consolidate the palette, read and follow with UISettings, set the DWM attribute to TRUE, and repaint everything including the look of the common controls.

Option 2 tends to end up “mostly dark, but light in places”, because the app cannot fully control the drawing of Win32 common controls (scroll bars, headers, tree expand buttons, and so on). As the official guide says, “the entire surface needs to follow”;3 a half-done dark mode is a worse experience than fixed light. For existing assets, choose option 1, declare “this app displays in light mode”, and switch to SetColorMode when you migrate to .NET 10. That is the realistic policy, and the easiest to explain.

WinForms options by runtimeOn .NET 10 or later use SetColorMode, on .NET 9 use the same API with WFO5001 suppressed, and on .NET 8 or earlier or .NET Framework choose between declaring fixed light and implementing full support yourself down to the common controls.NET 10 or later.NET 9.NET 8 or earlier / .NET FrameworkRecommendedIf you are prepared for itRuntime?SetColorMode (System)SetColorMode + suppress WFO5001What to do?Declare fixed lightFull support yourselfCommon controls remain

Figure 13: On runtimes without SetColorMode, declaring fixed light is the realistic default.

7. Implementation in WPF

7.1. .NET 9/10 — The Fluent Theme and ThemeMode

WPF on .NET 9 ships a new theme that follows the Fluent design of Windows 11, with support for light/dark and the accent color. There are two ways to apply it: set the ThemeMode property, or add the PresentationFramework.Fluent resource dictionary to MergedDictionaries.8

ThemeMode takes four values, Light / Dark / System / None (the default; the classic Aero2 theme). Set on Application it affects the whole app; set on a Window it affects only that window.8

<Application x:Class="OrderEntry.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             StartupUri="MainWindow.xaml"
             ThemeMode="System">
</Application>

ThemeMode does not only load the Fluent theme dictionaries into the resources; the documentation states that it “also controls the application of backdrop material and dark mode to the window”. In other words, WPF takes care of the DWM attribute from Chapter 4. ThemeMode and Resources are also designed to work in sync, which the documentation explains is to avoid an inconsistency where the window is dark but the controls inside are light.10

What ThemeMode in WPF doesSetting ThemeMode to System loads the Fluent theme dictionaries matching the Windows setting into the resources and also controls the application of dark mode and backdrop material to the windowThemeMode=SystemRead the Windows settingLoad the Fluent dictionaries into the resourcesDarken the window and apply the backdropStay in sync with Resources to avoid inconsistency

Figure 14: ThemeMode controls loading the Fluent dictionaries and darkening the window together.

There are two cautions to know before adopting it. First, reading and writing ThemeMode from code is an experimental feature, and accessing it produces the WPF0001 error. If you suppress it you can write Application.Current.ThemeMode = ThemeMode.Dark, but the API reference still carries [Experimental("WPF0001")] in .NET 10, with the note that it “may be removed in the future”.810 Second, Fluent style coverage is still “in progress” in .NET 10. .NET 10 added styles for DatePicker, GridSplitter, GroupBox, TextBox, and others, and fixed HighContrast-related crashes,9 which, read the other way, means the Fluent theme in .NET 9 lacked them. Before deciding to adopt it, confirm on a real machine that the controls your business app uses (DataGrid and third-party controls in particular) do not break under Fluent.

7.2. Following the Theme on the Classic Theme — Swapping ResourceDictionaries

In WPF without Fluent (or on .NET 8 or earlier, or .NET Framework), there is no standard dark mode support. The Win32 system colors do not change with the light/dark setting, so referencing WPF’s SystemColors will not make anything dark. The structure for following the theme yourself is as follows.

  1. Define colors and brushes with the same keys in Themes/Light.xaml for light and Themes/Dark.xaml for dark.
  2. Reference them from XAML with DynamicResource, as in {DynamicResource App.WindowBackgroundBrush} (StaticResource is fixed at load time and does not follow a swap).
  3. On a notification from the SystemThemeWatcher of Chapter 5, swap the corresponding dictionary in MergedDictionaries. During a contrast theme, whichever dictionary you put in, the trigger from Section 8.4 replaces the colors with system colors, so put in the light one.
public static class AppTheme
{
    private static readonly Uri Light = new("pack://application:,,,/Themes/Light.xaml");
    private static readonly Uri Dark = new("pack://application:,,,/Themes/Dark.xaml");

    public static void Apply(ThemeState state)
    {
        var merged = Application.Current.Resources.MergedDictionaries;
        var current = merged.FirstOrDefault(d => d.Source == Light || d.Source == Dark);
        // The dark dictionary only when dark. During a contrast theme, defer to the system colors (Section 8.4)
        var next = new ResourceDictionary { Source = state == ThemeState.Dark ? Dark : Light };
        if (current is null)
        {
            merged.Add(next);
        }
        else
        {
            merged[merged.IndexOf(current)] = next; // swap in place, at the same position
        }
    }
}
Swapping resource dictionaries in WPFDefine colors with the same keys in a light and a dark dictionary, reference them from XAML with DynamicResource, and swap the dictionary in MergedDictionaries on a theme change notification so that the references updateTheme change notificationSwap the dictionary in MergedDictionariesLight.xaml (same keys)Dark.xaml (same keys)DynamicResource references updateStaticResource referencesKeep the value from load time

Figure 15: Swap dictionaries with the same keys, and only DynamicResource references follow.

The templates of the standard controls (button backgrounds, scroll-bar colors) carry the classic theme’s colors, so here too there will be places where “the app’s own surfaces are dark but the standard controls are light”. Estimate the work of overriding the style of every control you need, then compare it against adopting Fluent or fixing light.

7.3. The Title Bar

If you use ThemeMode, WPF takes care of it. If you follow the theme yourself on the classic theme, use the DWM attribute setting from OnSourceInitialized shown in Section 4.2, and set it again on notifications from SystemThemeWatcher.

8. Drawing under a Contrast Theme — Keep the System-Color Pairs

8.1. Detection and Notification

In Win32, pass SPI_GETHIGHCONTRAST to SystemParametersInfo to receive a HIGHCONTRAST structure, and test the HCF_HIGHCONTRASTON bit of dwFlags. cbSize must be set before the call.1128 Microsoft positions this as “the only supported way to check whether high contrast is on”.12

bool IsContrastThemeActive()
{
    HIGHCONTRASTW hc{};
    hc.cbSize = sizeof(hc);
    if (!::SystemParametersInfoW(SPI_GETHIGHCONTRAST, sizeof(hc), &hc, 0))
    {
        // Do not hide a failure behind a default value. Surface it with the error code so the cause can be found
        throw std::system_error(::GetLastError(), std::system_category(),
                                "SystemParametersInfo(SPI_GETHIGHCONTRAST)");
    }
    return (hc.dwFlags & HCF_HIGHCONTRASTON) != 0;
}

Each framework has a property that wraps this call.

Environment Detection Change notification
Win32 / MFC SPI_GETHIGHCONTRAST + HCF_HIGHCONTRASTON WM_SYSCOLORCHANGE, WM_THEMECHANGED
WinForms SystemInformation.HighContrast SystemEvents.UserPreferenceChanged
WPF SystemParameters.HighContrast (maps to SPI_GETHIGHCONTRAST) SystemParameters.StaticPropertyChanged
WinUI 3 ThemeSettings.HighContrast (Microsoft.UI.System) ThemeSettings.Changed

The WinForms accessibility walkthrough asks you to check HighContrast at startup and respond to changes through UserPreferenceChanged.13 WPF’s SystemParameters.HighContrast maps to SPI_GETHIGHCONTRAST and HCF_HIGHCONTRASTON,29 and changes to the static properties are announced through StaticPropertyChanged.30 WinUI 3’s ThemeSettings is created bound to a window with CreateForWindowId and you subscribe to its Changed event, but note that the events stop unless you keep holding a reference to the object.31

Contrast theme detection pathsWin32 SPI_GETHIGHCONTRAST is the only supported detection method, and SystemInformation.HighContrast in WinForms, SystemParameters.HighContrast in WPF, and ThemeSettings.HighContrast in WinUI 3 are provided by each framework as wrappers around itSPI_GETHIGHCONTRAST (the only detection method)WinForms SystemInformationWPF SystemParametersWinUI 3 ThemeSettingsWin32: call it directly

Figure 16: The root of detection is a single Win32 API, and each framework has a property that wraps it.

8.2. Drawing Principles — Foreground and Background Pairs

Microsoft’s “High contrast parameter” lists three things an app should do when high contrast is on.11

  1. Map every color to one pair of foreground and background colors. Use GetSysColor with the pair COLOR_WINDOWTEXT and COLOR_WINDOW, or the pair COLOR_BTNTEXT and COLOR_BTNFACE.
  2. Omit bitmap images displayed behind text. They are a visual obstacle for users who need high contrast.
  3. Draw multicolor images in the foreground and background colors used for text.

The “pair” is the crux. The Windows 8 and later guide explains that COLOR_HIGHLIGHTTEXT is designed to be combined with the COLOR_HIGHLIGHT background and COLOR_WINDOWTEXT with the COLOR_WINDOW background, and asks you not to hardcode text colors and, because users customize the colors, to build a UI that does not depend on the theme in effect.12 The same guide’s example, “in Aero, text is always black and the selection color is light blue, but in High Contrast Black the selection color is black. If you assume black text and use the system selection color, you get black text on black”, is exactly the “status display vanished” complaint from the opening.

The Windows 11 contrast-theme guidance tabulates the pairings.1

Use Foreground Background
Headings, body text, lists, borders, non-interactive UI SystemColorWindowText SystemColorWindow
Hyperlinks SystemColorHotlight SystemColorWindow
Disabled or inactive UI SystemColorGrayText SystemColorWindow
Selected, hover, pressed, in progress SystemColorHighlightText SystemColorHighlight
Interactive UI such as buttons SystemColorButtonText SystemColorButtonFace

The things not to do are also spelled out. Do not use GrayText for supplementary or hint text (it is for disabled state only); do not use Hotlight for anything but hyperlinks; do not mix incompatible foregrounds and backgrounds; do not pick colors by appearance (users really do change them). There is also a design guideline that the backgrounds of pages, panes, and popups are based on SystemColorWindow, so adjacent surfaces end up with the same background color, and only the boundaries that matter are separated with a border used only under contrast themes (2px is recommended for flyouts and dialogs).1

How breaking the pair makes text unreadableAssuming text is black and using the system selection color only for the selection background gives black on black in High Contrast Black, where the selection color is black. Taking foreground and background as a pair keeps text readable even when the user edits the colorsAssume text is blackOnly the selection background uses the system selection colorIn High Contrast Black the selection color is blackBlack text on blackTake foreground and background as a pairReadable even when the user edits the colorsUser edits the colors

Figure 17: Using a system color for only one side can give black on black, but taking the pair stays readable even when the colors are edited.

Drawing decisions under a contrast themeWhen a contrast theme is active, map colors to the system-color pairs, omit images behind text, draw multicolor images in the two foreground and background colors, and do not use hardcoded colorsContrast theme activeMap colors to pairsOmit images behind textDraw multicolor graphics in two colorsNo hardcoded colors

Figure 18: Drawing under an active contrast theme comes down to four points: mapping, omission, two colors, and no hardcoding.

8.3. Implementation in WinForms

The standard WinForms controls follow the system colors as long as ForeColor / BackColor are left at their defaults. Only the places with custom colors and the owner drawing are switched according to the check. The walkthrough’s example takes a label that is yellow on blue normally and reverts it to SystemColors.Window / SystemColors.WindowText under high contrast.13 It amounts to adding a contrast-theme branch to the palette structure from earlier.

using Microsoft.Win32;

public partial class OrderForm : Form
{
    private readonly SynchronizationContext _ui;

    public OrderForm()
    {
        InitializeComponent();
        // The controls exist now, so a WindowsFormsSynchronizationContext is in place
        _ui = SynchronizationContext.Current
              ?? throw new InvalidOperationException("Create this form on the UI thread.");
        ApplyColorScheme();
        SystemEvents.UserPreferenceChanged += OnUserPreferenceChanged;
    }

    private void ApplyColorScheme()
    {
        if (SystemInformation.HighContrast)
        {
            // Keep the pair, defer entirely to the system colors, and remove the image behind the text
            statusLabel.BackColor = SystemColors.Window;
            statusLabel.ForeColor = SystemColors.WindowText;
            headerPanel.BackgroundImage = null;
        }
        else
        {
            var p = AppPalette.Current; // the light/dark palette (Chapter 5)
            statusLabel.BackColor = p.PanelBackground;
            statusLabel.ForeColor = p.PanelForeground;
            headerPanel.BackgroundImage = Properties.Resources.HeaderPattern;
        }
    }

    private void OnUserPreferenceChanged(object? sender, UserPreferenceChangedEventArgs e)
    {
        // This event is not guaranteed to arrive on the UI thread either. Marshal to the UI, then re-evaluate without filtering by category
        _ui.Post(_ =>
        {
            if (IsDisposed) return;
            ApplyColorScheme();
        }, null);
    }

    // A static event, so the form leaks unless it is detached. Detach in Dispose(bool), which also runs
    // on the paths where the form is disposed without being closed (if the designer-generated Dispose(bool) exists, put it there)
    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            SystemEvents.UserPreferenceChanged -= OnUserPreferenceChanged;
        }
        base.Dispose(disposing);
    }
}

Owner drawing in OnPaint uses system brushes that keep the pair, such as SystemBrushes.Window / SystemPens.WindowText, and multicolor graphics such as a colored dot that represents a status are replaced with a border and text (“Running”, “Stopped”) in the foreground color. Conveying information by more than color alone is the same point as Success Criterion 1.4.1 covered in the previous article.

Branching of the WinForms color schemeApplyColorScheme is called at startup and on every UserPreferenceChanged; if SystemInformation.HighContrast is true it defers to the system-color pairs and removes the background image, and if false it takes colors from the light/dark paletteTrueFalseStartup / UserPreferenceChangedApplyColorSchemeSystemInformation.HighContrast?Defer to the SystemColors pairsRemove the background imageTake colors from the light/dark palette

Figure 19: In WinForms the same routine is called at startup and on every notification, and under a contrast theme it defers to the system colors.

8.4. Implementation in WPF

WPF’s SystemColors update automatically when a brush changes if you reference a resource key such as WindowBrushKey through DynamicResource (a static reference that uses WindowBrush directly does not update).32 To change the appearance only during a contrast theme, reference the value of SystemParameters.HighContrast from a DataTrigger. However, SystemParameters.HighContrast is a static property, so on its own it is not a live binding source. Prepare one small proxy that subscribes to StaticPropertyChanged, holds the value, and notifies through INotifyPropertyChanged, and bind with that instance as the Source.30

public sealed class ThemeSettings : INotifyPropertyChanged
{
    public static ThemeSettings Instance { get; } = new();

    public bool IsHighContrast { get; private set; } = SystemParameters.HighContrast;
    public event PropertyChangedEventHandler? PropertyChanged;

    private ThemeSettings()
    {
        // Raised when a static property of SystemParameters changes (SPI_GETHIGHCONTRAST is read again)
        SystemParameters.StaticPropertyChanged += (_, e) =>
        {
            if (!string.IsNullOrEmpty(e.PropertyName)
                && e.PropertyName != nameof(SystemParameters.HighContrast)) return;
            IsHighContrast = SystemParameters.HighContrast;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsHighContrast)));
        };
    }
}
<!-- Declare xmlns:local="clr-namespace:OrderEntry" beforehand -->
<Style x:Key="CardStyle" TargetType="Border">
    <Setter Property="Background" Value="{DynamicResource App.CardBackgroundBrush}"/>
    <Setter Property="BorderBrush" Value="{DynamicResource App.CardBorderBrush}"/>
    <Setter Property="BorderThickness" Value="1"/>
    <Style.Triggers>
        <DataTrigger Binding="{Binding Source={x:Static local:ThemeSettings.Instance}, Path=IsHighContrast}"
                     Value="True">
            <!-- Keep the pair: background is Window, border and text are WindowText. Make the boundary thicker -->
            <Setter Property="Background"
                    Value="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"/>
            <Setter Property="BorderBrush"
                    Value="{DynamicResource {x:Static SystemColors.WindowTextBrushKey}}"/>
            <!-- Let the text inside inherit it too. Note that a child that sets Foreground explicitly cuts off the inheritance -->
            <Setter Property="TextElement.Foreground"
                    Value="{DynamicResource {x:Static SystemColors.WindowTextBrushKey}}"/>
            <Setter Property="BorderThickness" Value="2"/>
        </DataTrigger>
    </Style.Triggers>
</Style>
WPF system color references and the contrast theme triggerReferencing SystemColors resource keys through DynamicResource follows brush changes automatically, and a trigger bound to IsHighContrast on a proxy that subscribes to StaticPropertyChanged reacts to a switch at runtime and switches to colors that keep the pair. A direct reference to WindowBrush does not updateReference WindowBrushKey via DynamicResourceFollows brush changes automaticallyStaticPropertyChangedIsHighContrast on the proxyDataTrigger reactsSwitch to colors that keep the pairReference WindowBrush directlyDoes not update

Figure 20: WPF follows a switch at runtime through dynamic references to resource keys and a binding to a proxy that relays changes to the static property.

If you use the Fluent theme, keep in mind that .NET 10 included HighContrast-related crash fixes,9 and make verification under a contrast theme a condition of adoption.

8.5. Implementation in WinUI 3

In WinUI 3, the standard controls follow light, dark, and contrast themes from the start, and the app’s own colors are defined in ResourceDictionary.ThemeDictionaries under the keys Default (dark), Light, and HighContrast. Under HighContrast, do not hardcode colors; reference dynamic system colors such as SystemColorWindowColor through ThemeResource. A custom control that has Light/Dark must always have HighContrast as well, and HighContrast is the fallback key used when no other named high-contrast theme is found.331

<ResourceDictionary.ThemeDictionaries>
    <ResourceDictionary x:Key="Default">
        <SolidColorBrush x:Key="App.CardBackgroundBrush" Color="#2B2B2B"/>
    </ResourceDictionary>
    <ResourceDictionary x:Key="Light">
        <SolidColorBrush x:Key="App.CardBackgroundBrush" Color="#F3F3F3"/>
    </ResourceDictionary>
    <ResourceDictionary x:Key="HighContrast">
        <SolidColorBrush x:Key="App.CardBackgroundBrush"
                         Color="{ThemeResource SystemColorWindowColor}"/>
    </ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>

One more thing: WinUI has a mechanism called HighContrastAdjustment, enabled by default. It forces white text and a black highlight background to preserve contrast, and the guidance recommends that once you have prepared theme dictionaries that use the system colors correctly, set it to None so that your own styles apply.1

How WinUI resolves ThemeDictionariesDepending on the current theme, one of the Default (dark), Light, and HighContrast dictionaries is selected, and under HighContrast dynamic system colors are referenced through ThemeResource. HighContrast is the fallback key when there is no named high-contrast themeDarkLightContrast themeCurrent theme?Default dictionaryLight dictionaryHighContrast dictionaryReference SystemColor resources via ThemeResourceFallback when there is no named theme

Figure 21: In WinUI the dictionary for each theme is selected automatically, and the HighContrast dictionary references the system colors.

9. Dark Mode Support Is Not a Substitute for Accessibility Support

Reporting dark mode support as “we did the accessibility work” is a mistake. The relationship between the two can be laid out as follows.

  • The contrast-ratio standard applies to the dark palette just the same. WCAG Success Criterion 1.4.3 requires 4.5:1 for text and 3:1 for large text, and that does not change when the background is dark.14 A dark design that puts mid-gray text on a dark gray background has the same problem as “light gray on white” in light mode.
  • Avoiding pure black and pure white is the Windows 11 design. Microsoft’s best practices explain that Windows 11 moved away from pure white and pure black to tones that are easier on the eyes.34 Conversely, a #000000 background in dark mode leads some people to complain of halation, a blooming effect caused by excessive contrast against bright text.
  • Accommodating color-vision diversity is needed regardless of theme. Microsoft’s color guidance asks you to use color as visual reinforcement rather than the primary means of communication, and never to make the combination of red and green the only distinction.3515
  • Contrast themes are a requirement independent of dark mode. As Chapter 2 explained, dark mode is unavailable while a contrast theme is active, so however perfect your dark mode support is, it never reaches users of contrast themes.
The relationship between dark mode and accessibilityDark mode support is a matter of visual preference and environment, while the accessibility requirements of contrast ratio, conveying information by more than color alone, and contrast theme support must be met separately regardless of themeNot a substitute forDark mode supportPreference and environmentAccessibilityContrast ratio 4.5:1Not color aloneContrast theme support

Figure 22: Dark mode support addresses preference and environment; the accessibility requirements must be met separately.

On the other hand, the work of “gathering colors in one place” from Chapter 5 is the foundation of both. With the palette in one place, you can enumerate what to measure for contrast ratio in both light and dark, and the contrast-theme branch can be written in the same place. Use dark mode support as the occasion to consolidate the palette, and inspect contrast ratios and contrast themes while you are at it. That is the order with the best return on the investment.

10. Deciding a Policy — Recommendations by App Type

App type Recommended policy
New WinForms (.NET 10) Use SetColorMode(System). Base owner drawing on SystemColors, and opt custom controls that contain common controls in with ApplyThemingImplicitly
New WPF (.NET 9/10) Adopt ThemeMode="System" after checking how the controls you use render and how the app behaves under contrast themes. If that is difficult, classic theme plus dictionary swapping
Existing WinForms/WPF (.NET Framework 4.8, .NET 8 or earlier) Declare “fixed light” and leave the DWM attribute at its default (FALSE). Contrast theme support is mandatory; move to dark mode support when migrating to .NET 10
WinUI 3 Follows the system by default. Define the app’s own colors in ThemeDictionaries including HighContrast, and set HighContrastAdjustment to None
Win32 / MFC DWM attribute + your own palette + recomputation on WM_THEMECHANGED / WM_SYSCOLORCHANGE. The official guide covers only detection and the title bar; repainting the common controls is out of its scope
The path from existing assets to dark mode supportExisting assets first declare fixed light, complete contrast theme support without fail, consolidate the palette as preparation, then migrate to .NET 10 and switch to SetColorMode or ThemeMode. A half-done dark mode is a worse experience than fixed light, so that path is not takenNot takenDeclare fixed light (now)Contrast theme support (mandatory)Consolidate the palette (preparation)Migrate to .NET 10Switch to SetColorMode / ThemeModeHalf-done dark modeWorse experience than fixed light

Figure 23: Existing assets start from fixed light, go through contrast theme support and palette consolidation, and move to dark mode support when migrating to .NET 10.

“Fixed light” is not a defeat. It is the default behavior of Windows itself, and documented behavior. An app that is consistently light is far better for users than a half-done dark mode that ships with “only the title bar black” or “only the scroll bars white”. However, contrast theme support is the one thing that cannot be “fixed”. It falls under the reasonable accommodation covered in the previous article: it is a matter of whether the app is usable, not a matter of theme preference.

11. Verification Checklist

Once the work is done, verify on a real machine in the following order. Every step can be switched from the Settings app in a matter of seconds.

  1. Switch light/dark while the app is running. Change the mode under Settings > Personalization > Colors, and confirm that both the title bar and the client area follow, or that the app behaves as specified with “takes effect at the next launch” (SetColorMode in WinForms does not follow).
  2. Trigger a handle recreation. In WinForms, toggle ShowInTaskbar at runtime and confirm that the title-bar attribute is preserved.
  3. Try all four contrast themes. Toggle with left Alt + left Shift + Print Screen, and confirm in each of Aquatic, Desert, Dusk, and Night sky that text, borders, selected rows, disabled items, and links are readable.1
  4. Edit the colors of a contrast theme. Users really do change the colors. Edit the background to an extreme color to flush out any remaining hardcoded colors.
  5. Check the logs. Confirm that a failure of DwmSetWindowAttribute or SystemParametersInfo on a supported OS is recorded, and that on Windows 10 below build 22000 the app starts light without calling the DWM attribute.
  6. Measure the contrast ratios. In both light and dark, check every combination of text and background color in the palette against 4.5:1.14
Verification steps for theme supportVerify on a real machine in this order: switching light/dark at runtime, handle recreation, the four contrast themes, editing the theme colors, checking failure logs, and measuring contrast ratiosSwitch light/dark at runtimeHandle recreationThe four contrast themesEdit the theme colorsCheck the failure logsMeasure the contrast ratios

Figure 24: Verification starts with switching settings and closes with checking the logs and contrast ratios.

12. Summary

  • Windows themes have two axes, light/dark and contrast themes, and dark mode is unavailable while the latter is active. Detect the contrast theme first.
  • An existing app’s title bar is white because that is the compatibility default; pass TRUE for DWMWA_USE_IMMERSIVE_DARK_MODE (value 20, Windows 11 build 22000 or later) via DwmSetWindowAttribute, and it is drawn dark when the system is dark. Set it every time the HWND is created, and log failures.
  • Decide the current mode from the brightness of the foreground color from UISettings.GetColorValue, notice changes with ColorValuesChanged, marshal to the UI thread, and repaint. Keep the colors gathered in one place.
  • WinForms: Application.SetColorMode(SystemColorMode.System) on .NET 9/10. Know the three constraints (Windows 11 only, disabled during a contrast theme, no tracking of changes at runtime) and ApplyThemingImplicitly for custom controls.
  • WPF: ThemeMode="System" on .NET 9/10. Manipulation from code is experimental and Fluent is in progress, so evaluate before adopting. On the classic theme, dictionary swapping plus DynamicResource.
  • Under a contrast theme, detect with the SPI_GETHIGHCONTRAST family, map colors to the system-color pairs, omit images behind text, and draw multicolor graphics in two colors. GrayText is for disabled state, Hotlight for links only.
  • Dark mode support is not a substitute for accessibility support. 4.5:1 still applies in dark mode, information must not rely on color alone, and contrast theme support is required separately.
  • For existing assets, declaring “fixed light” is the realistic answer, and contrast theme support is the one thing that cannot be fixed.

As a recommended first step, pick one main screen, first enable a contrast theme with left Alt + left Shift + Print Screen and look at it, switch back with the same keys, and then switch the Windows color setting to dark (dark mode is unavailable while a contrast theme is active, so try the two separately). Within a few minutes you will see “where your app keeps its colors”.

KomuraSoft LLC handles dark mode support for WinForms/WPF business apps (consolidating the palette, evaluating a migration to SetColorMode / ThemeMode on .NET 9/10, integrating the DWM attribute), diagnosing and fixing display breakage under contrast themes, and consultations on theme tracking in Win32/MFC assets. Starting from the stage of “employees complained once they switched to dark mode” is fine.

References

  1. Microsoft Learn, Contrast themes. That contrast themes use a constrained palette of roughly 7:1 or higher and must not be confused with light and dark themes; the four themes Aquatic, Desert, Dusk, and Night sky and editing their colors; toggling with left Alt + left Shift + Print Screen; the foreground/background pairs and uses of the SystemColor resources; using GrayText for disabled state only and Hotlight for links only; breakage from hardcoded colors; boundary borders; HighContrast in ThemeDictionaries; setting HighContrastAdjustment to None; and detection with Microsoft.UI.System.ThemeSettings.  2 3 4 5 6 7 8

  2. Microsoft Learn, Application.SetColorMode(SystemColorMode) Method. On calling it before UI elements are created, the app not adapting automatically when the system setting changes even with System, and the dark color mode being available only on Windows 11 or later and unavailable in high contrast mode.  2 3 4

  3. Microsoft Learn, Support Dark and Light themes in Win32 apps. On the definition of foreground and background in the color modes; that Windows gives a light title bar by default for compatibility because it cannot know whether an app supports dark mode; the procedure of getting the foreground color with UISettings.GetColorValue and classifying light or dark by perceived luminance to detect dark mode; tracking with ColorValuesChanged; enabling the dark title bar with DwmSetWindowAttribute and DWMWA_USE_IMMERSIVE_DARK_MODE (value 20); and that the entire surface needs to follow dark mode.  2 3 4 5 6 7 8 9 10

  4. Microsoft Learn, DWMWINDOWATTRIBUTE enumeration (dwmapi.h). That DWMWA_USE_IMMERSIVE_DARK_MODE allows the frame to be drawn dark when the system’s dark setting is enabled and that all windows default to light; the COLORREF values of DWMWA_BORDER_COLOR, DWMWA_CAPTION_COLOR, and DWMWA_TEXT_COLOR and restoring the default with DWMWA_COLOR_DEFAULT; support from Windows 11 build 22000; and support for DWMWA_SYSTEMBACKDROP_TYPE from build 22621.  2 3 4

  5. Microsoft Learn, UISettings.ColorValuesChanged Event. On the event raised when a color value changes.  2

  6. Microsoft Learn, What’s new in Windows Forms for .NET 9. On the experimental preliminary dark mode support, SystemColors changing accordingly when the color mode changes, the three SystemColorMode values Classic, System, and Dark, calling Application.SetColorMode in the startup code, and suppressing WFO5001.  2 3

  7. Microsoft Learn, What’s new in Windows Forms for .NET 10. On the full integration of dark mode and SetColorMode no longer being experimental, Win32 common controls inside owner-drawn controls staying light unless they opt in, and the need to call SetStyle(ControlStyles.ApplyThemingImplicitly) before base.CreateParams inside the CreateParams override because the constructor is too late.  2 3 4

  8. Microsoft Learn, What’s new in WPF for .NET 9. On the Fluent theme supporting light/dark and the accent color, the four ThemeMode values Light, Dark, System, and None and setting it on Application or Window, applying it through resource dictionaries, and setting ThemeMode from code being experimental and requiring suppression of WPF0001.  2 3 4

  9. Microsoft Learn, What’s new in WPF for .NET 10. That Fluent UI style support is still in progress, the added Fluent styles for DatePicker, GridSplitter, GridView, GroupBox, Hyperlink, Label, NavigationWindow, RichTextBox, and TextBox, and the HighContrast-related crash fixes.  2 3

  10. Microsoft Learn, Application.ThemeMode Property. That it controls whether the Fluent theme is loaded in light, dark, or system mode and also controls the application of backdrop material and dark mode to the window, that ThemeMode and Resources are designed to stay in sync to avoid inconsistency, and that it carries the Experimental(“WPF0001”) attribute and may be removed in the future.  2 3 4

  11. Microsoft Learn, High contrast parameter. On getting the HIGHCONTRAST structure with SPI_GETHIGHCONTRAST at initialization and when handling WM_SYSCOLORCHANGE and checking HCF_HIGHCONTRASTON, and, when it is on, mapping every color to one pair of COLOR_WINDOWTEXT and COLOR_WINDOW or COLOR_BTNTEXT and COLOR_BTNFACE, omitting bitmap images behind text, and drawing multicolor images in the foreground and background colors.  2 3

  12. Microsoft Learn, High-contrast mode. That Aero has black text and a light blue selection color but High Contrast Black has a black selection color, which can produce black text on black; that COLOR_HIGHLIGHTTEXT is designed to be paired with COLOR_HIGHLIGHT and COLOR_WINDOWTEXT with COLOR_WINDOW; not hardcoding text colors; building a UI that does not depend on the theme because users customize the colors; recomputing colors on WM_THEMECHANGED; and SPI_GETHIGHCONTRAST being the only supported way to check.  2 3

  13. Microsoft Learn, Walkthrough: Creating an Accessible Windows-based Application. On detection with SystemInformation.HighContrast; using the system color scheme when it is on, adding visual cues to information conveyed by color, and omitting images behind text; checking at startup and following the UserPreferenceChanged event; and the example of switching a label’s colors with SystemColors.  2 3

  14. W3C / Japanese translation by the Web Accessibility Infrastructure Committee (WAIC), Web Content Accessibility Guidelines (WCAG) 2.1, Japanese translation. On Success Criterion 1.4.3 (Contrast (Minimum)) with 4.5:1 for text and 3:1 for large text, and Success Criterion 1.4.1 (Use of Color).  2 3

  15. Microsoft Learn, Color in Windows. That Windows has two color modes, light and dark, that the accent color and theme choice are reflected across the user’s whole experience, and on ensuring contrast and accommodating color-vision diversity.  2

  16. Microsoft Learn, Reference for Windows 11 and Windows 10 settings. That AppsUseLightTheme and SystemUsesLightTheme under HKCU\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize are DWORD values representing the light/dark mode of apps and of Windows. 

  17. Microsoft Learn, Theming in Windows apps. That removing RequestedTheme makes the app follow the system setting, that the system overrides RequestedTheme when the user chooses a high contrast theme, and that custom templates should use theme brushes rather than hardcoded colors. 

  18. Microsoft Learn, DwmSetWindowAttribute function (dwmapi.h). On the function that sets the DWM rendering attributes of a window’s non-client area, and its availability from Windows Vista. 

  19. Microsoft Learn, Retrieve a window handle (HWND). On getting the Handle from WindowInteropHelper in WPF. 

  20. Microsoft Learn, DWM_SYSTEMBACKDROP_TYPE enumeration (dwmapi.h). That DWMSBT_MAINWINDOW corresponds to Mica and DWMSBT_TRANSIENTWINDOW to Acrylic on Windows 11, that the effect of the material may change in future versions of Windows, and support from Windows 11 build 22621. 

  21. Microsoft Learn, UISettings.GetColorValue(UIColorType) Method. On the method that returns the color value of the specified UIColorType. 

  22. Microsoft Learn, WM_SETTINGCHANGE message. On the message sent to all top-level windows when SystemParametersInfo changes a system-wide setting or when a policy setting changes. 

  23. Microsoft Learn, SystemEvents.UserPreferenceChanged Event. On the static event raised when a user preference changes, and the memory leak that results from not detaching the handler. 

  24. Microsoft Learn, WM_THEMECHANGED message. That it is broadcast to all windows after a theme is activated, deactivated, or switched, and that existing theme handles become invalid and must be reopened. 

  25. Microsoft Learn, WM_SYSCOLORCHANGE message. That it is sent to all top-level windows when a system color setting changes, that brushes that use system colors must be recreated, and that it must be forwarded to the common controls. 

  26. Microsoft Learn, Compiler Error WFO5001. That SetColorMode and SystemColorMode were guarded as experimental features for evaluation in .NET 9, and that the error does not apply from .NET 10. 

  27. Microsoft Learn, SystemColors.UseAlternativeColorSet Property. That setting it to true makes the system KnownColor values return an alternative color set (currently the dark mode version), that it is an experimental feature under SYSLIB5002, and that the system KnownColor values always return the current Windows colors when a high contrast theme is active in Windows. 

  28. Microsoft Learn, HIGHCONTRASTW structure (winuser.h). On HCF_HIGHCONTRASTON (0x00000001) in dwFlags, and the need to specify cbSize when using it with SPI_GETHIGHCONTRAST. 

  29. Microsoft Learn, SystemParameters.HighContrast Property. On the static WPF property that maps to SPI_GETHIGHCONTRAST and HCF_HIGHCONTRASTON. 

  30. Microsoft Learn, SystemParameters.StaticPropertyChanged Event. On the static event raised when any property of SystemParameters changes.  2

  31. Microsoft Learn, ThemeSettings Class (Microsoft.UI.System). On creating it bound to a window with CreateForWindowId and receiving high contrast changes through the Changed event, and that releasing the reference destroys the object and the event no longer fires. 

  32. Microsoft Learn, SystemColors.WindowBrushKey Property. That a dynamic reference made with the resource key updates automatically when the brush changes, and that a static reference through WindowBrush does not. 

  33. Microsoft Learn, ResourceDictionary.ThemeDictionaries Property (Microsoft.UI.Xaml). That a custom control with Light and Dark theme dictionaries should also provide a HighContrast dictionary, that HighContrast is the fallback key when no other high contrast theme exists, that Default is used when no ResourceDictionary for the theme is found, and that system color resources such as SystemColorButtonFaceColor can be used in HighContrast. 

  34. Microsoft Learn, Windows app development best practices. That Windows 11 moved away from pure white and pure black to tones that are easier on the eyes, and that the dark and light themes are a means of adapting to the user’s visual preference. 

  35. Microsoft Learn, Color (Windows UX guidelines). On using color as visual reinforcement rather than the primary means of communication, choosing theme colors and system colors by purpose and using foreground and background in matching pairs, handling theme changes with WM_THEMECHANGED, and High Contrast Black corresponding to Aquatic and High Contrast White to Desert on Windows 11. 

Recent articles sharing the same tags. Deepen your understanding with closely related topics.

These topic pages place the article in a broader service and decision context.

This article connects naturally to the following service pages.

Frequently Asked Questions

Common questions about the topic of this article.

I switched Windows to dark mode, but the title bar of our in-house WinForms app is still white. Why?
Because Windows has no way of knowing whether an app supports dark mode, so for compatibility it treats every window as light mode by default. The non-client area, including the title bar, is drawn by the Desktop Window Manager (DWM), and the frame is drawn dark when the system is dark only after the app passes TRUE for DWMWA_USE_IMMERSIVE_DARK_MODE (value 20) through DwmSetWindowAttribute. Support for this attribute is documented for Windows 11 build 22000 and later. If you use Application.SetColorMode in WinForms on .NET 9 or later, or ThemeMode in WPF on .NET 9 or later, the framework makes this call for you, so calling it yourself is only needed in apps on .NET 8 or earlier, .NET Framework, or Win32/MFC. Note that a dark title bar over a client area that stays white looks worse, not better. Enable this attribute only when you are ready to repaint the whole app dark.
Are dark mode and contrast themes (high contrast) the same thing?
No. Light/dark is the color mode under Settings > Personalization > Colors, and it uses a broad palette that swaps the brightness of foreground and background. A contrast theme is chosen under Settings > Accessibility > Contrast themes and uses a constrained palette with a contrast ratio of roughly 7:1 or higher (the four built-in themes Aquatic, Desert, Dusk, and Night sky, plus any colors the user has edited). Microsoft's documentation explicitly says not to confuse the two, and dark mode is unavailable while a contrast theme is active (SetColorMode in WinForms does not provide dark mode during a contrast theme, and RequestedTheme in XAML is overridden by the system). In your implementation, first check whether a contrast theme is active and, if so, defer entirely to the system colors; only otherwise choose the light or dark palette. That is the order of precedence.
What is the shortest way to make a WinForms app support dark mode?
On .NET 9 or later, the shortest path is to call Application.SetColorMode(SystemColorMode.System) before Application.Run in Program.cs. On .NET 9 it was an experimental feature, so WFO5001 had to be suppressed in the project file; from .NET 10 it works without suppression. Calling SetColorMode switches SystemColors to an alternative set for dark mode, and the standard controls are drawn accordingly. There are three caveats. First, dark mode is available only on Windows 11 or later and is disabled while a contrast theme is active. Second, even with SystemColorMode.System, the app does not follow a change to the Windows setting while it is running (the change takes effect at the next launch). Third, if an owner-drawn control uses Win32 common controls such as scroll bars, you must override CreateParams and call SetStyle(ControlStyles.ApplyThemingImplicitly, true) before base.CreateParams (the constructor is too late).
What does a WPF app need in order to follow dark mode?
WPF on .NET 9 or later ships a new theme that follows the Fluent design of Windows 11, and writing ThemeMode="System" on the Application element in App.xaml is enough to load the Fluent theme that matches the light/dark setting of Windows. ThemeMode also controls darkening the window (the title bar) and applying the backdrop material. However, reading and writing the ThemeMode property from code is still experimental in .NET 10 (WPF0001), and the Fluent styles themselves are described as "still in progress" in the .NET 10 documentation. Before adopting it in a business app, evaluate whether the controls you use render correctly under Fluent. If you stay on the classic theme (the same applies to .NET 8 and earlier and to .NET Framework), prepare a light and a dark ResourceDictionary, swap them in MergedDictionaries, reference them from XAML with DynamicResource, and use UISettings.ColorValuesChanged to detect the switch. For the title bar, get the HWND from WindowInteropHelper in SourceInitialized and call DwmSetWindowAttribute.
Why does text disappear or become unreadable under a contrast theme (high contrast)?
The typical causes are hardcoded colors, or breaking the pairing of foreground and background system colors. Under a contrast theme the user can freely edit the background, text, link, and other colors, so every assumption like "text will be black" or "the selected row will be light blue" falls apart. For example, if only the background is fixed at #E6E6E6, some themes give a white foreground, and white text on light gray becomes unreadable. There are three principles. Detect the state with SPI_GETHIGHCONTRAST (SystemInformation.HighContrast in WinForms, SystemParameters.HighContrast in WPF); replace every color with the correct system-color pair (WindowText with Window, ButtonText with ButtonFace, HighlightText with Highlight); and drop images behind text and multicolor graphics, drawing with the foreground and background colors only. GrayText must not be used for anything but disabled state, and Hotlight for anything but hyperlinks. Changes are announced by WM_SYSCOLORCHANGE and WM_THEMECHANGED (SystemEvents.UserPreferenceChanged in .NET), so recompute the colors there and repaint.

Author Profile

Profile page for the article author.

Go Komura

Representative of KomuraSoft LLC

Focused on Windows software development, technical consulting, and investigations into failures that are difficult to reproduce.

Back to the Blog