Let's be honest — if your app doesn't support dark mode in 2026, users notice. They expect modern mobile apps to respect their system appearance preferences, and increasingly, they want full control over the look and feel. Whether your users prefer a dark interface to save battery on OLED screens, a light theme for outdoor readability, or a completely custom color palette, your .NET MAUI app needs to handle all of these gracefully.
In this guide, you'll build a complete theming system from scratch. We'll cover how to detect and follow the OS theme automatically, create custom light and dark resource dictionaries, switch themes at runtime, persist the user's choice across sessions, and leverage the .NET MAUI Community Toolkit for cleaner theme-aware code. Every approach includes working code you can drop into a .NET MAUI 10 project today.
Understanding the .NET MAUI Theming Architecture
Before writing any code, it helps to understand the three core building blocks .NET MAUI gives you for theming:
- AppThemeBinding — a XAML markup extension that automatically resolves a value based on the current system theme (light or dark). No event handlers needed.
- DynamicResource — a markup extension that maintains a live link to a
ResourceDictionarykey. When you swap the dictionary at runtime, every control referencing that key updates instantly. - UserAppTheme — a property on
Application.Currentthat lets you override the system theme programmatically, forcing the entire app into a specific mode.
These three mechanisms can be combined, and honestly, that's where the real power lies. In a typical production app, you'd use AppThemeBinding for straightforward light/dark toggles, DynamicResource for fully custom multi-theme palettes, and UserAppTheme to let the user lock the app into a specific mode regardless of what the OS says.
Approach 1: AppThemeBinding for System-Aware Theming
The simplest way to support dark mode in .NET MAUI is AppThemeBinding. It works on iOS 13+, Android 10+ (API 29), macOS 10.14+, and Windows 10+, and requires zero event handling — the framework handles the UI updates for you when the system theme changes.
Inline AppThemeBinding
You can apply AppThemeBinding directly on any property:
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
BackgroundColor="{AppThemeBinding Light=#FFFFFF, Dark=#1E1E1E}">
<VerticalStackLayout Padding="20" Spacing="16">
<Label Text="Welcome to the app"
FontSize="24"
TextColor="{AppThemeBinding Light=#1A1A2E, Dark=#E0E0E0}" />
<BoxView HeightRequest="1"
Color="{AppThemeBinding Light=#E0E0E0, Dark=#333333}" />
<Button Text="Get Started"
BackgroundColor="{AppThemeBinding Light=#6200EE, Dark=#BB86FC}"
TextColor="{AppThemeBinding Light=#FFFFFF, Dark=#000000}" />
</VerticalStackLayout>
</ContentPage>
This is the fastest way to add dark mode support, but it has a pretty obvious drawback: hard-coded hex values scattered across every XAML file become a nightmare to maintain at scale.
AppThemeBinding with Named Resources
A better approach is to define named colors in your App.xaml resource dictionary, then reference them inside AppThemeBinding:
<!-- App.xaml -->
<Application.Resources>
<ResourceDictionary>
<Color x:Key="LightBackground">#FFFFFF</Color>
<Color x:Key="DarkBackground">#1E1E1E</Color>
<Color x:Key="LightPrimary">#6200EE</Color>
<Color x:Key="DarkPrimary">#BB86FC</Color>
<Color x:Key="LightText">#1A1A2E</Color>
<Color x:Key="DarkText">#E0E0E0</Color>
</ResourceDictionary>
</Application.Resources>
<!-- Usage in any page -->
<Label Text="Hello"
TextColor="{AppThemeBinding Light={StaticResource LightText},
Dark={StaticResource DarkText}}" />
This centralizes all your color definitions while still leveraging automatic system theme detection. Much cleaner.
AppThemeBinding in Styles
For maximum reusability, define AppThemeBinding inside implicit or explicit styles:
<Style TargetType="Label">
<Setter Property="TextColor"
Value="{AppThemeBinding Light={StaticResource LightText},
Dark={StaticResource DarkText}}" />
</Style>
<Style TargetType="ContentPage">
<Setter Property="BackgroundColor"
Value="{AppThemeBinding Light={StaticResource LightBackground},
Dark={StaticResource DarkBackground}}" />
</Style>
Now every Label and ContentPage in your app automatically adopts the right colors without any per-element markup. This is the approach I'd recommend for most projects.
Approach 2: Custom ResourceDictionary Themes with Runtime Switching
When you need more than just light and dark — maybe a high-contrast mode, a branded theme for enterprise customers, or a "pure black" AMOLED theme — you need to go beyond AppThemeBinding and build custom ResourceDictionary files.
This is where things get interesting.
Step 1: Create Theme Resource Dictionaries
Create a Themes folder inside your Resources directory. Add separate XAML files for each theme:
<!-- Resources/Themes/LightTheme.xaml -->
<ResourceDictionary xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MyApp.Resources.Themes.LightTheme">
<Color x:Key="PageBackground">#FFFFFF</Color>
<Color x:Key="CardBackground">#F5F5F5</Color>
<Color x:Key="PrimaryColor">#6200EE</Color>
<Color x:Key="SecondaryColor">#03DAC6</Color>
<Color x:Key="PrimaryText">#1A1A2E</Color>
<Color x:Key="SecondaryText">#666666</Color>
<Color x:Key="DividerColor">#E0E0E0</Color>
<Color x:Key="ErrorColor">#B00020</Color>
</ResourceDictionary>
<!-- Resources/Themes/DarkTheme.xaml -->
<ResourceDictionary xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MyApp.Resources.Themes.DarkTheme">
<Color x:Key="PageBackground">#121212</Color>
<Color x:Key="CardBackground">#1E1E1E</Color>
<Color x:Key="PrimaryColor">#BB86FC</Color>
<Color x:Key="SecondaryColor">#03DAC6</Color>
<Color x:Key="PrimaryText">#E0E0E0</Color>
<Color x:Key="SecondaryText">#AAAAAA</Color>
<Color x:Key="DividerColor">#333333</Color>
<Color x:Key="ErrorColor">#CF6679</Color>
</ResourceDictionary>
<!-- Resources/Themes/AmoledTheme.xaml -->
<ResourceDictionary xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MyApp.Resources.Themes.AmoledTheme">
<Color x:Key="PageBackground">#000000</Color>
<Color x:Key="CardBackground">#0A0A0A</Color>
<Color x:Key="PrimaryColor">#BB86FC</Color>
<Color x:Key="SecondaryColor">#03DAC6</Color>
<Color x:Key="PrimaryText">#FFFFFF</Color>
<Color x:Key="SecondaryText">#B0B0B0</Color>
<Color x:Key="DividerColor">#1A1A1A</Color>
<Color x:Key="ErrorColor">#CF6679</Color>
</ResourceDictionary>
Step 2: Reference Colors with DynamicResource
In your pages, use DynamicResource instead of StaticResource so the UI actually updates when the theme dictionary gets swapped:
<ContentPage BackgroundColor="{DynamicResource PageBackground}">
<Frame BackgroundColor="{DynamicResource CardBackground}"
CornerRadius="12"
Padding="16">
<VerticalStackLayout Spacing="8">
<Label Text="Card Title"
FontSize="18"
FontAttributes="Bold"
TextColor="{DynamicResource PrimaryText}" />
<Label Text="Supporting description text goes here."
TextColor="{DynamicResource SecondaryText}" />
<Button Text="Action"
BackgroundColor="{DynamicResource PrimaryColor}"
TextColor="White" />
</VerticalStackLayout>
</Frame>
</ContentPage>
Step 3: Build a Theme Service
Now for the core piece — a service class that handles loading themes and sets the default on startup:
public enum AppThemeOption
{
Light,
Dark,
Amoled,
System
}
public class ThemeService
{
private const string ThemePreferenceKey = "app_theme";
public void ApplyTheme(AppThemeOption theme)
{
// Save the preference
Preferences.Default.Set(ThemePreferenceKey, theme.ToString());
if (theme == AppThemeOption.System)
{
Application.Current!.UserAppTheme = AppTheme.Unspecified;
var systemTheme = Application.Current.PlatformAppTheme;
LoadThemeDictionary(systemTheme == AppTheme.Dark
? new Resources.Themes.DarkTheme()
: new Resources.Themes.LightTheme());
return;
}
ResourceDictionary themeDictionary = theme switch
{
AppThemeOption.Dark => new Resources.Themes.DarkTheme(),
AppThemeOption.Amoled => new Resources.Themes.AmoledTheme(),
_ => new Resources.Themes.LightTheme()
};
// Lock UserAppTheme so AppThemeBinding-based values also update
Application.Current!.UserAppTheme = theme switch
{
AppThemeOption.Dark or AppThemeOption.Amoled => AppTheme.Dark,
_ => AppTheme.Light
};
LoadThemeDictionary(themeDictionary);
}
public AppThemeOption GetSavedTheme()
{
var saved = Preferences.Default.Get(ThemePreferenceKey, "System");
return Enum.TryParse<AppThemeOption>(saved, out var result)
? result
: AppThemeOption.System;
}
private static void LoadThemeDictionary(ResourceDictionary theme)
{
var mergedDictionaries = Application.Current!.Resources.MergedDictionaries;
// Remove any existing theme dictionary
var existing = mergedDictionaries
.FirstOrDefault(d => d.GetType().Namespace?.Contains("Themes") == true);
if (existing is not null)
mergedDictionaries.Remove(existing);
mergedDictionaries.Add(theme);
}
}
Step 4: Register and Initialize the Service
Register ThemeService in your DI container and apply the saved theme during startup:
// MauiProgram.cs
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
builder.Services.AddSingleton<ThemeService>();
return builder.Build();
}
// App.xaml.cs
public partial class App : Application
{
public App(ThemeService themeService)
{
InitializeComponent();
themeService.ApplyTheme(themeService.GetSavedTheme());
}
}
Pretty straightforward, right? The DI container takes care of injecting the service, and the saved preference gets applied before any page renders.
Responding to System Theme Changes at Runtime
Here's something that catches a lot of developers off guard: when the user toggles their device from light to dark mode while your app is running, you need to react to that change. Subscribe to the RequestedThemeChanged event:
// App.xaml.cs
public partial class App : Application
{
private readonly ThemeService _themeService;
public App(ThemeService themeService)
{
InitializeComponent();
_themeService = themeService;
_themeService.ApplyTheme(_themeService.GetSavedTheme());
RequestedThemeChanged += OnRequestedThemeChanged;
}
private void OnRequestedThemeChanged(object? sender, AppThemeChangedEventArgs e)
{
// Only react if the user chose "System" mode
if (_themeService.GetSavedTheme() == AppThemeOption.System)
{
_themeService.ApplyTheme(AppThemeOption.System);
}
}
}
Android note: To receive theme change events on Android, you must include ConfigChanges.UiMode in the Activity attribute on your MainActivity. Miss this and you'll spend an hour wondering why your event handler never fires (ask me how I know):
[Activity(Theme = "@style/Maui.SplashTheme",
MainLauncher = true,
ConfigurationChanges = ConfigChanges.ScreenSize
| ConfigChanges.Orientation
| ConfigChanges.UiMode
| ConfigChanges.ScreenLayout
| ConfigChanges.SmallestScreenSize
| ConfigChanges.Density)]
public class MainActivity : MauiAppCompatActivity { }
Building a Theme Settings Page
Let's give your users an intuitive settings screen where they can pick their preferred theme. Here's a complete implementation using the ThemeService from the previous section:
<!-- ThemeSettingsPage.xaml -->
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MyApp.ThemeSettingsPage"
Title="Appearance"
BackgroundColor="{DynamicResource PageBackground}">
<VerticalStackLayout Padding="20" Spacing="12">
<Label Text="Choose Theme"
FontSize="20"
FontAttributes="Bold"
TextColor="{DynamicResource PrimaryText}" />
<CollectionView x:Name="ThemeList"
SelectionMode="Single"
SelectionChanged="OnThemeSelected">
<CollectionView.ItemTemplate>
<DataTemplate>
<Frame BackgroundColor="{DynamicResource CardBackground}"
Padding="16"
CornerRadius="8"
Margin="0,4">
<HorizontalStackLayout Spacing="12">
<BoxView WidthRequest="24"
HeightRequest="24"
CornerRadius="12"
Color="{Binding PreviewColor}" />
<VerticalStackLayout>
<Label Text="{Binding DisplayName}"
FontSize="16"
TextColor="{DynamicResource PrimaryText}" />
<Label Text="{Binding Description}"
FontSize="13"
TextColor="{DynamicResource SecondaryText}" />
</VerticalStackLayout>
</HorizontalStackLayout>
</Frame>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</VerticalStackLayout>
</ContentPage>
// ThemeSettingsPage.xaml.cs
public partial class ThemeSettingsPage : ContentPage
{
private readonly ThemeService _themeService;
public ThemeSettingsPage(ThemeService themeService)
{
InitializeComponent();
_themeService = themeService;
var options = new List<ThemeOption>
{
new("System Default", "Follow your device settings", Colors.Gray, AppThemeOption.System),
new("Light", "Bright background with dark text", Colors.White, AppThemeOption.Light),
new("Dark", "Easy on the eyes in low light", Colors.DimGray, AppThemeOption.Dark),
new("AMOLED Dark", "Pure black for OLED screens", Colors.Black, AppThemeOption.Amoled),
};
ThemeList.ItemsSource = options;
var currentTheme = _themeService.GetSavedTheme();
ThemeList.SelectedItem = options.FirstOrDefault(o => o.Theme == currentTheme);
}
private void OnThemeSelected(object? sender, SelectionChangedEventArgs e)
{
if (e.CurrentSelection.FirstOrDefault() is ThemeOption selected)
{
_themeService.ApplyTheme(selected.Theme);
}
}
}
public record ThemeOption(
string DisplayName,
string Description,
Color PreviewColor,
AppThemeOption Theme);
Using AppThemeColor from the Community Toolkit
If you're already using the CommunityToolkit.Maui package (version 9.0+), there's a nice shortcut: AppThemeColor and AppThemeResource. They reduce boilerplate significantly compared to standard AppThemeBinding.
Install the Package
dotnet add package CommunityToolkit.Maui
Register the Toolkit
// MauiProgram.cs
builder.UseMauiApp<App>()
.UseMauiCommunityToolkit();
Define AppThemeColor Resources
Instead of creating two separate color entries per semantic color, you define a single AppThemeColor that wraps both light and dark values together:
<ContentPage.Resources>
<toolkit:AppThemeColor x:Key="PageBg" Light="#FFFFFF" Dark="#1E1E1E" />
<toolkit:AppThemeColor x:Key="CardBg" Light="#F5F5F5" Dark="#2C2C2C" />
<toolkit:AppThemeColor x:Key="TextPrimary" Light="#1A1A2E" Dark="#E0E0E0" />
<toolkit:AppThemeColor x:Key="Accent" Light="#6200EE" Dark="#BB86FC" />
</ContentPage.Resources>
Consume with AppThemeResource
<ContentPage BackgroundColor="{toolkit:AppThemeResource PageBg}">
<Frame BackgroundColor="{toolkit:AppThemeResource CardBg}">
<Label Text="Clean and simple"
TextColor="{toolkit:AppThemeResource TextPrimary}" />
</Frame>
</ContentPage>
This cuts the number of resource entries in half and eliminates the verbose AppThemeBinding Light=..., Dark=... syntax. For simple light/dark scenarios where you don't need custom multi-theme support, this is arguably the cleanest option available.
Platform-Specific Considerations
Each platform has its own quirks that can trip you up when implementing theming. Here are the critical ones worth knowing about:
Android
- ConfigChanges.UiMode must be set on your
MainActivityActivityattribute for theRequestedThemeChangedevent to fire. This one is easy to forget. - The status bar and navigation bar colors don't automatically follow your theme. Use the Community Toolkit's
StatusBarBehaviorto set them per page. - AMOLED dark mode (#000000 background) saves measurable battery on OLED panels — consider offering it as a distinct option. Your users with Samsung devices will thank you.
iOS
- iOS automatically adjusts system UI elements (status bar text, keyboard appearance) based on the
UserInterfaceStyle. SettingUserAppThemein .NET MAUI propagates correctly to these system elements. - If you override the theme, the Info.plist key
UIUserInterfaceStyleshould not be set, or it'll conflict with your runtime override. I've seen this cause some really confusing bugs.
Windows
- Window title bar theming requires additional handler customization through
Microsoft.UI.Xaml.WindowAPIs. - Preferences key names on Windows are limited to 255 characters — keep your theme preference keys short.
Theming Images and Icons
Colors are only part of the picture (pun intended). Icons, illustrations, and splash screens also need to adapt. AppThemeBinding works with ImageSource properties too:
<Image Source="{AppThemeBinding Light=logo_light.png, Dark=logo_dark.png}"
HeightRequest="64" />
<Image Source="{AppThemeBinding Light=hero_illustration_light.svg,
Dark=hero_illustration_dark.svg}" />
For vector icons using font icon packs (like Material Design Icons), you can theme them by binding the TextColor of an Image rendered from a font glyph to a themed color resource.
Performance Tips for Dynamic Theming
A few things worth keeping in mind as your app grows:
- Use StaticResource when possible. If a resource doesn't need to change at runtime (like font sizes or corner radii that stay the same across themes), use
StaticResourceinstead ofDynamicResource. Dynamic resolution has measurable overhead on large visual trees. - Minimize MergedDictionaries churn. When switching themes, remove only the old theme dictionary before adding the new one. Don't call
Clear()on all merged dictionaries — that would nuke your non-theme dictionaries too. - Batch resource updates. If you're switching multiple dictionaries, add them in a single operation to avoid intermediate layout passes.
- Profile with .NET MAUI diagnostics. .NET 10 introduced layout performance metrics through OpenTelemetry. Use them to verify that theme switches aren't causing excessive measure-and-arrange cycles.
Frequently Asked Questions
How do I set dark mode as the default in .NET MAUI?
Set Application.Current.UserAppTheme = AppTheme.Dark in your App constructor before any pages are loaded. This forces the entire application into dark mode regardless of the OS setting. To let users override this later, store their preference with Preferences.Default.Set() and apply it on startup.
What's the difference between AppThemeBinding and DynamicResource for theming?
AppThemeBinding automatically switches between two predefined values (light and dark) based on the system theme. DynamicResource is more flexible — it references a dictionary key that you can replace with any value at any time. Use AppThemeBinding for simple light/dark support and DynamicResource when you need more than two themes or want full runtime control over the color palette.
Does .NET MAUI dark mode work on all platforms?
System theme detection and AppThemeBinding work on iOS 13+, Android 10+ (API 29), macOS 10.14+, and Windows 10+. Older platform versions fall back to the Default value you provide. The runtime ResourceDictionary swap approach has no platform version restrictions — it works everywhere .NET MAUI runs.
How do I change the Android status bar color when switching themes?
Use the StatusBarBehavior from the .NET MAUI Community Toolkit. Add it as a page behavior and bind its StatusBarColor and StatusBarStyle properties to your theme resources. This handles both the background color and the icon/text style (light or dark content) for the status bar.
Can I use AppThemeBinding and DynamicResource together?
Absolutely. A common pattern is to use AppThemeBinding for simple elements like text colors and backgrounds, while reserving DynamicResource with swappable ResourceDictionary files for controls or pages that need more than two theme variants. The two mechanisms work independently and coexist without any conflict.