Dark Mode and Contrast Themes in Windows Apps — DWM Dark Title Bars, System Theme Tracking in WinForms/WPF, and Drawing under High Contrast
· Updated: · Go Komura · 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.
flowchart TB
accTitle: The flow of this article
accDescr: The 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 verification
axes["Sort out the two theme axes"] --> why["Why the default is light"]
why --> dwm["DWM dark title bar"]
dwm --> detect["Detection and tracking"]
detect --> impl["WinForms/WPF implementation"]
impl --> hc["Drawing under a contrast theme"]
hc --> policy["Deciding 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) viaDwmSetWindowAttribute. 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.GetColorValueand receive changes throughColorValuesChanged. 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.SetColorModein .NET 9, and it stopped being experimental in .NET 10. Call it withSystemColorMode.SystembeforeApplication.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
ThemeModein .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.HighContrastin WinForms,SystemParameters.HighContrastin WPF); notifications areWM_SYSCOLORCHANGE/WM_THEMECHANGED(SystemEvents.UserPreferenceChangedin .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.
flowchart TB
accTitle: The two theme axes and the order of precedence
accDescr: The 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 palette
q1{"Contrast theme active?"}
q1 -->|Yes| sys["Defer to the system-color pairs"]
q1 -->|No| q2{"Color mode?"}
q2 -->|Light| light["Light palette"]
q2 -->|Dark| dark["Dark palette"]
sys -.-> note["Dark 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
flowchart TB
accTitle: The two areas of a window and who draws them
accDescr: The 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 both
win["Top-level window"] --> nc["Non-client area (title bar, frame)"]
win --> client["Client area (the window contents)"]
nc --> dwm["Composed and drawn by DWM"]
client --> app["Drawn by the app or framework"]
dwm -.-> attr["Instructed via DwmSetWindowAttribute"]
app -.-> palette["The 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.
flowchart TB
accTitle: How the default becomes light
accDescr: Windows 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 setting
unknown["Windows cannot tell whether the app supports it"] --> def["Default is light, for compatibility"]
def --> q{"Did the app pass TRUE?"}
q -->|No| light["Always a light frame"]
q -->|Yes| follow["Drawn 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.
flowchart TB
accTitle: When to call DwmSetWindowAttribute
accDescr: Set 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 arrives
create["HWND created"] --> apply["Set the DWM attribute"]
apply --> run["Displayed"]
run -->|Handle recreated| create
run -->|Theme change notification| apply
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.
flowchart TB
accTitle: How to decide the title-bar color
accDescr: Following 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_DEFAULT
q{"Title-bar color?"}
q -->|Follow the system default| dark["Just set value 20 to TRUE"]
q -->|Paint a brand color| brand["Specify the color attributes (34 to 36)"]
brand --> resp["Guarantee text and button contrast yourself"]
brand --> states["Manage active/inactive yourself"]
brand -.-> reset["Restore 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.
flowchart TB
accTitle: The three steps of detection and tracking
accDescr: The 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 palette
read["Read: UISettings.GetColorValue"] --> paint["Repaint: reapply the palette"]
notice["Notice: ColorValuesChanged"] --> ui["Marshal to the UI thread"]
ui --> read
paint -.-> dwm["Set 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.
flowchart TB
accTitle: Ways to read the color mode
accDescr: The 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 diagnostics
q["Light or dark right now?"] --> uis["UISettings.GetColorValue"]
q -.-> reg["Registry AppsUseLightTheme"]
uis --> fg["Judge the foreground's perceived luminance"]
fg --> ans["Bright means dark mode"]
reg -.-> diag["Storage 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.
flowchart TB
accTitle: Theme change notification paths and threads
accDescr: ColorValuesChanged 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 routine
cvc["ColorValuesChanged"] --> marshal["Marshal to the UI thread"]
upc["UserPreferenceChanged"] --> reapply["Read and repaint"]
wm["WM_THEMECHANGED etc."] --> reapply
marshal --> reapply
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.
flowchart TB
accTitle: The structure that gathers colors in one place
accDescr: The 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 enumerated
mode["Current mode"] --> sel{"Which one?"}
sel -->|Light| pl["Light palette"]
sel -->|Dark| pd["Dark palette"]
pl --> screens["Each screen and control"]
pd --> screens
hard["Scattered Color.White"] -.-> cannot["Cannot 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
- The dark color mode is available only on Windows 11 or later.
- Dark mode is unavailable when a contrast theme is active.
- 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.
flowchart TB
accTitle: The flow and constraints of SetColorMode
accDescr: SetColorMode 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 runtime
sc["SetColorMode (System)"] --> before["Before Application.Run"]
before --> colors["SystemColors switches to the alternative set"]
colors --> ctrls["Standard controls follow"]
sc -.-> c1["Windows 11 only"]
sc -.-> c2["Disabled during a contrast theme"]
sc -.-> c3["Does 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;
}
}
}
flowchart TB
accTitle: When ApplyThemingImplicitly can be set
accDescr: ApplyThemingImplicitly 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 late
basector["Base-class constructor"] --> cp["Reads CreateParams"]
cp --> st["SetStyle must happen by here"]
st --> derived["Derived-class constructor"]
derived -.-> late["Calling 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.
- 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.
- 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.
flowchart TB
accTitle: WinForms options by runtime
accDescr: On .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
q{"Runtime?"}
q -->|.NET 10 or later| n10["SetColorMode (System)"]
q -->|.NET 9| n9["SetColorMode + suppress WFO5001"]
q -->|.NET 8 or earlier / .NET Framework| legacy{"What to do?"}
legacy -->|Recommended| fixed["Declare fixed light"]
legacy -->|If you are prepared for it| full["Full support yourself"]
full -.-> partial["Common 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
flowchart TB
accTitle: What ThemeMode in WPF does
accDescr: Setting 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 window
tm["ThemeMode=System"] --> read["Read the Windows setting"]
read --> dict["Load the Fluent dictionaries into the resources"]
read --> win["Darken the window and apply the backdrop"]
dict --> sync["Stay 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.
- Define colors and brushes with the same keys in
Themes/Light.xamlfor light andThemes/Dark.xamlfor dark. - Reference them from XAML with DynamicResource, as in
{DynamicResource App.WindowBackgroundBrush}(StaticResourceis fixed at load time and does not follow a swap). - On a notification from the
SystemThemeWatcherof Chapter 5, swap the corresponding dictionary inMergedDictionaries. 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
}
}
}
flowchart TB
accTitle: Swapping resource dictionaries in WPF
accDescr: Define 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 update
notify["Theme change notification"] --> swap["Swap the dictionary in MergedDictionaries"]
light["Light.xaml (same keys)"] --> swap
dark["Dark.xaml (same keys)"] --> swap
swap --> dyn["DynamicResource references update"]
static["StaticResource references"] -.-> stale["Keep 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
flowchart TB
accTitle: Contrast theme detection paths
accDescr: Win32 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 it
spi["SPI_GETHIGHCONTRAST (the only detection method)"] --> wf["WinForms SystemInformation"]
spi --> wpf["WPF SystemParameters"]
spi --> winui["WinUI 3 ThemeSettings"]
spi --> win32["Win32: 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
- Map every color to one pair of foreground and background colors. Use
GetSysColorwith the pairCOLOR_WINDOWTEXTandCOLOR_WINDOW, or the pairCOLOR_BTNTEXTandCOLOR_BTNFACE. - Omit bitmap images displayed behind text. They are a visual obstacle for users who need high contrast.
- 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
flowchart TB
accTitle: How breaking the pair makes text unreadable
accDescr: Assuming 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 colors
assume["Assume text is black"] --> hl["Only the selection background uses the system selection color"]
hl --> black["In High Contrast Black the selection color is black"]
black --> broken["Black text on black"]
pair["Take foreground and background as a pair"] --> ok["Readable even when the user edits the colors"]
edit["User edits the colors"] -.-> assume
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.
flowchart TB
accTitle: Drawing decisions under a contrast theme
accDescr: When 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 colors
on["Contrast theme active"] --> map["Map colors to pairs"]
on --> img["Omit images behind text"]
on --> multi["Draw multicolor graphics in two colors"]
on --> nohard["No 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.
flowchart TB
accTitle: Branching of the WinForms color scheme
accDescr: ApplyColorScheme 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 palette
start["Startup / UserPreferenceChanged"] --> apply["ApplyColorScheme"]
apply --> q{"SystemInformation.HighContrast?"}
q -->|True| sys["Defer to the SystemColors pairs"]
sys --> noimg["Remove the background image"]
q -->|False| pal["Take 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>
flowchart TB
accTitle: WPF system color references and the contrast theme trigger
accDescr: Referencing 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 update
key["Reference WindowBrushKey via DynamicResource"] --> auto["Follows brush changes automatically"]
spc["StaticPropertyChanged"] --> proxy["IsHighContrast on the proxy"]
proxy --> trig["DataTrigger reacts"]
trig --> pair["Switch to colors that keep the pair"]
direct["Reference WindowBrush directly"] -.-> stale["Does 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
flowchart TB
accTitle: How WinUI resolves ThemeDictionaries
accDescr: Depending 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 theme
theme{"Current theme?"}
theme -->|Dark| def["Default dictionary"]
theme -->|Light| light["Light dictionary"]
theme -->|Contrast theme| hc["HighContrast dictionary"]
hc --> sysc["Reference SystemColor resources via ThemeResource"]
hc -.-> fb["Fallback 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
#000000background 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.
flowchart TB
accTitle: The relationship between dark mode and accessibility
accDescr: Dark 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 theme
dark["Dark mode support"] --> pref["Preference and environment"]
a11y["Accessibility"] --> ratio["Contrast ratio 4.5:1"]
a11y --> sole["Not color alone"]
a11y --> ct["Contrast theme support"]
dark -.->|Not a substitute for| a11y
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 |
flowchart TB
accTitle: The path from existing assets to dark mode support
accDescr: Existing 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 taken
now["Declare fixed light (now)"] --> ct["Contrast theme support (mandatory)"]
ct --> pal["Consolidate the palette (preparation)"]
pal --> mig["Migrate to .NET 10"]
mig --> dark["Switch to SetColorMode / ThemeMode"]
now -.->|Not taken| half["Half-done dark mode"]
half -.-> worse["Worse 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.
- 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” (
SetColorModein WinForms does not follow). - Trigger a handle recreation. In WinForms, toggle
ShowInTaskbarat runtime and confirm that the title-bar attribute is preserved. - 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
- 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.
- Check the logs. Confirm that a failure of
DwmSetWindowAttributeorSystemParametersInfoon a supported OS is recorded, and that on Windows 10 below build 22000 the app starts light without calling the DWM attribute. - 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
flowchart TB
accTitle: Verification steps for theme support
accDescr: Verify 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 ratios
s1["Switch light/dark at runtime"] --> s2["Handle recreation"]
s2 --> s3["The four contrast themes"]
s3 --> s4["Edit the theme colors"]
s4 --> s5["Check the failure logs"]
s5 --> s6["Measure 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) viaDwmSetWindowAttribute, 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 withColorValuesChanged, 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) andApplyThemingImplicitlyfor 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_GETHIGHCONTRASTfamily, 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”.
Related Articles
- An Introduction to Windows App Accessibility — Preparing for UI Automation and Reasonable-Accommodation Requirements
- WinRT Is COM — IInspectable, .winmd, Language Projections, and Why WinUI Still Rests on a Binary Contract
- Calling Win32 APIs Safely from C# — A Practical P/Invoke Guide (DllImport / LibraryImport / CsWin32)
- The UI Thread and async/await in WPF/WinForms, on One Sheet
- High-DPI Support in WinForms — Why the UI Blurs or Breaks on 4K Monitors, and Practical Fixes
- WPF High-DPI Support — Why It Still Blurs and Bleeds Despite Being “Supposedly DPI-Aware”, and How to Fix It
- Choosing between WinForms, WPF, and WinUI - A Practical Decision Table
Related Consulting Areas
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
-
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
-
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
-
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
-
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
-
Microsoft Learn, UISettings.ColorValuesChanged Event. On the event raised when a color value changes. ↩ ↩2
-
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
-
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
-
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
-
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
-
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
-
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
-
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
-
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
-
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
-
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
-
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. ↩
-
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. ↩
-
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. ↩
-
Microsoft Learn, Retrieve a window handle (HWND). On getting the Handle from WindowInteropHelper in WPF. ↩
-
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. ↩
-
Microsoft Learn, UISettings.GetColorValue(UIColorType) Method. On the method that returns the color value of the specified UIColorType. ↩
-
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. ↩
-
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. ↩
-
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. ↩
-
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. ↩
-
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. ↩
-
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. ↩
-
Microsoft Learn, HIGHCONTRASTW structure (winuser.h). On HCF_HIGHCONTRASTON (0x00000001) in dwFlags, and the need to specify cbSize when using it with SPI_GETHIGHCONTRAST. ↩
-
Microsoft Learn, SystemParameters.HighContrast Property. On the static WPF property that maps to SPI_GETHIGHCONTRAST and HCF_HIGHCONTRASTON. ↩
-
Microsoft Learn, SystemParameters.StaticPropertyChanged Event. On the static event raised when any property of SystemParameters changes. ↩ ↩2
-
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. ↩
-
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. ↩
-
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. ↩
-
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. ↩
-
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. ↩
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
Windows App Accessibility Basics — UI Automation and Preparing for Mandatory Reasonable Accommodation
How screen readers read Windows apps via UI Automation: WinForms/WPF naming, keyboard, contrast, and verification tools, under Japan's am...
End of Servicing for Windows Printer Drivers — How Business Apps Should Prepare Their Report and Label Printing
Microsoft is phasing out v3/v4 printer drivers. What Windows protected print mode removes, and how to inventory and prepare report and la...
Windows App Outsourcing and Custom Software Development: What to Sort Out Before You Ask
Before commissioning Windows app outsourcing or custom software development, here is how to sort out existing software modification, devi...
What "Not Responding" Really Is — How Windows Decides an App Has Hung, and How to Design Apps That Don't
Windows marks a window Not Responding after 5 seconds without message retrieval and shows a ghost window: the check, hang causes, UI-thre...
How the Clipboard and Drag & Drop Work — Handling OLE Data Transfer Correctly in Business Apps
Why Excel pastes break and paste fails once the source closes: clipboard formats, delayed rendering, OLE drag and drop, and clipboard his...
Related Topics
These topic pages place the article in a broader service and decision context.
Windows Technical Topics
Topic hub for KomuraSoft LLC's Windows development, investigation, and legacy-asset articles.
UI Threading & Timers
Topic page for WPF / WinForms UI threading, async flow, Dispatcher usage, and timer decisions.
Where This Topic Connects
This article connects naturally to the following service pages.
Windows App Development
We support Windows desktop applications that involve resident processing, device integration, operational logging, and maintainable structure.
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.