{"slug":"maui-theming","title":"maui-theming","summary":"Guide for theming .NET MAUI apps — light/dark mode via AppThemeBinding, ResourceDictionary theme switching, DynamicResource bindings, system theme detection, and user theme preferences. Use when: \"dark mode\", \"light mode\", \"theming\", \"AppThemeBinding\", \"theme switching\", \"Resourc","platform":"Claude","tags":[],"authorName":"LLM Mart","authorSlug":"llm-mart","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-08-24T05:37:30.898082Z","repo":{"url":"https://github.com/dotnet/skills","stars":5534,"forks":420,"license":"MIT","updatedAt":"2026-10-01T15:13:53Z"},"bodyHtml":"<hr>\n<h2>name: maui-theming\ndescription: &gt;-\nGuide for theming .NET MAUI apps — light/dark mode via AppThemeBinding,\nResourceDictionary theme switching, DynamicResource bindings, system theme\ndetection, and user theme preferences.\nUse when: \"dark mode\", \"light mode\", \"theming\", \"AppThemeBinding\",\n\"theme switching\", \"ResourceDictionary theme\", \"dynamic resources\",\n\"system theme detection\", \"color scheme\", \"app theme\", \"DynamicResource\".\nDo not use for: localization or language switching (see .NET MAUI localization\ndocumentation), accessibility visual adjustments (see .NET MAUI accessibility\ndocumentation), app icons or splash screens (see .NET MAUI app icons\ndocumentation), or Bootstrap-style class theming (see Plugin.Maui.BootstrapTheme\nNuGet package).\nlicense: MIT</h2>\n<h1>.NET MAUI Theming</h1>\n<p>Apply light/dark mode support, custom branded themes, and runtime theme switching in .NET MAUI apps using AppThemeBinding, ResourceDictionary swapping, and system theme detection APIs.</p>\n<h2>When to Use</h2>\n<ul>\n<li>Adding light and dark mode support to a .NET MAUI app</li>\n<li>Creating custom branded themes with ResourceDictionary</li>\n<li>Detecting and responding to system theme changes at runtime</li>\n<li>Letting users choose a preferred theme (light, dark, or system default)</li>\n<li>Combining OS-driven theme response with custom color palettes</li>\n</ul>\n<h2>When Not to Use</h2>\n<ul>\n<li>Localization or language switching — see <a href=\"https://learn.microsoft.com/dotnet/maui/fundamentals/localization\">.NET MAUI localization docs</a></li>\n<li>Accessibility-specific visual adjustments — see <a href=\"https://learn.microsoft.com/dotnet/maui/fundamentals/accessibility\">.NET MAUI accessibility docs</a></li>\n<li>App icon or splash screen configuration — see <a href=\"https://learn.microsoft.com/dotnet/maui/user-interface/images/app-icons\">.NET MAUI app icon docs</a></li>\n<li>Bootstrap-style class theming — see the <code>Plugin.Maui.BootstrapTheme</code> NuGet package</li>\n</ul>\n<h2>Inputs</h2>\n<ul>\n<li>A .NET MAUI project targeting .NET 8 or later</li>\n<li>XAML pages or C# UI code that need theme-aware styling</li>\n</ul>\n<h2>Workflow</h2>\n<ol>\n<li>Detect the current theme approach in the project (AppThemeBinding, ResourceDictionary, or none).</li>\n<li>Choose the appropriate strategy: AppThemeBinding for simple light/dark, ResourceDictionary swap for custom/multiple themes, or both combined.</li>\n<li>Define theme resources — inline <code>AppThemeBinding</code> values or separate <code>ResourceDictionary</code> files with matching keys.</li>\n<li>Replace hardcoded colors with <code>DynamicResource</code> bindings (or <code>AppThemeBinding</code> markup) throughout XAML pages.</li>\n<li>Add system theme detection via <code>Application.Current.RequestedTheme</code> and the <code>RequestedThemeChanged</code> event.</li>\n<li>Implement user preference persistence with <code>Preferences.Set</code> / <code>Preferences.Get</code> and apply on startup.</li>\n<li>Verify Android <code>ConfigChanges.UiMode</code> is set on <code>MainActivity</code> to avoid activity restarts on theme change.</li>\n<li>Test both light and dark themes on at least one target platform, confirming all UI elements respond correctly.</li>\n</ol>\n<h2>Rules That Change the Answer</h2>\n<p>Check these rules against the user's scenario, and apply <strong>only</strong> the ones that\naffect what they asked. <code>UiMode</code> and dictionary swapping matter for <em>runtime theme\nswitching</em>; they are noise in a question about setting up <code>AppThemeBinding</code>.</p>\n<p><strong>Answer narrowly, but completely.</strong> Completeness means showing the code that\nimplements <em>what you recommended</em> — not adding adjacent topics. If you recommend\n<code>DynamicResource</code>, show the dictionary swap that makes it update. If the user asks\nfor light/dark colours in C#, show <strong>both</strong> <code>SetAppThemeColor</code> (colours) and the\ngeneric <code>SetAppTheme&lt;T&gt;</code> (any bindable property type), and prefer resource keys over\nscattered hardcoded colours. Do not tack on platform configuration the question\ndidn't raise.</p>\n<table>\n<thead>\n<tr>\n<th>Rule</th>\n<th>Do this</th>\n<th>Not this</th>\n<th>Why</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Runtime-swapped values must be dynamic</strong></td>\n<td><code>{DynamicResource Key}</code></td>\n<td><code>{StaticResource Key}</code></td>\n<td><code>StaticResource</code> resolves once at load and never updates when dictionaries are swapped.</td>\n</tr>\n<tr>\n<td><strong>Android must declare <code>UiMode</code></strong> <em>(only for runtime/system theme switching)</em></td>\n<td>Include <code>ConfigChanges.UiMode</code> in the <code>ConfigurationChanges</code> list on <code>MainActivity</code></td>\n<td>Omitting it</td>\n<td>Without it Android restarts the activity on theme change — navigation state is lost and it looks like a crash. Irrelevant to a static <code>AppThemeBinding</code> setup</td>\n</tr>\n<tr>\n<td><strong>Force a theme via <code>UserAppTheme</code></strong></td>\n<td><code>Application.Current.UserAppTheme = AppTheme.Dark</code></td>\n<td>Manually re-assigning colors</td>\n<td><code>UserAppTheme</code> overrides the OS; <code>AppTheme.Unspecified</code> returns to following the system.</td>\n</tr>\n</tbody>\n</table>\n<p><strong>Do not</strong> replace a working <code>AppThemeBinding</code> setup with ResourceDictionary\nswapping (or vice versa) unless the user needs what the other approach provides —\nmore than two themes, or a user-selectable theme.</p>\n<h2>Choosing an Approach</h2>\n<table>\n<thead>\n<tr>\n<th>Approach</th>\n<th>Best for</th>\n<th>Limitation</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>AppThemeBinding</strong></td>\n<td>Automatic light/dark with OS — minimal code</td>\n<td>Only two themes (light + dark)</td>\n</tr>\n<tr>\n<td><strong>ResourceDictionary swap</strong></td>\n<td>Custom branded themes, more than two themes, user preference</td>\n<td>More setup; must use <code>DynamicResource</code> everywhere</td>\n</tr>\n<tr>\n<td><strong>Both combined</strong></td>\n<td>OS-driven response plus custom theme colors</td>\n<td>Most flexible but most complex</td>\n</tr>\n</tbody>\n</table>\n<h2>AppThemeBinding (OS Light/Dark)</h2>\n<p><code>AppThemeBinding</code> selects a value based on the current system theme. It supports <code>Light</code>, <code>Dark</code>, and an optional <code>Default</code> fallback.</p>\n<h3>Define the palette once — don't scatter literals</h3>\n<p>Putting <code>{AppThemeBinding Light=#333333, Dark=#FFFFFF}</code> on every element is the\nsingle most common theming mistake: the palette ends up duplicated across dozens of\nfiles and cannot be changed in one place. <strong>Recommend this shape as the final\nanswer</strong>, not inline literals:</p>\n<pre><code>&lt;!-- App.xaml — one source of truth for the whole app --&gt;\n&lt;Application.Resources&gt;\n    &lt;ResourceDictionary&gt;\n\n        &lt;!-- 1. Raw palette --&gt;\n        &lt;Color x:Key=\"LightPageBackground\"&gt;#FFFFFF&lt;/Color&gt;\n        &lt;Color x:Key=\"DarkPageBackground\"&gt;#1E1E1E&lt;/Color&gt;\n        &lt;Color x:Key=\"LightPrimaryText\"&gt;#333333&lt;/Color&gt;\n        &lt;Color x:Key=\"DarkPrimaryText\"&gt;#E0E0E0&lt;/Color&gt;\n\n        &lt;!-- 2. Implicit styles bind the pair once; every page inherits them --&gt;\n        &lt;Style TargetType=\"ContentPage\" ApplyToDerivedTypes=\"True\"&gt;\n            &lt;Setter Property=\"BackgroundColor\"\n                    Value=\"{AppThemeBinding Light={StaticResource LightPageBackground},\n                                            Dark={StaticResource DarkPageBackground}}\" /&gt;\n        &lt;/Style&gt;\n\n        &lt;Style TargetType=\"Label\"&gt;\n            &lt;Setter Property=\"TextColor\"\n                    Value=\"{AppThemeBinding Light={StaticResource LightPrimaryText},\n                                            Dark={StaticResource DarkPrimaryText}}\" /&gt;\n        &lt;/Style&gt;\n\n    &lt;/ResourceDictionary&gt;\n&lt;/Application.Resources&gt;\n</code></pre>\n<p>Pages then need <strong>no theming markup at all</strong> — they pick the styles up implicitly.\nUse an inline <code>AppThemeBinding</code> only for genuine one-offs, and even then reference\n<code>{StaticResource}</code> keys rather than literal hex.</p>\n<h3>XAML (inline form, for one-offs)</h3>\n<pre><code>&lt;Label Text=\"Themed text\"\n       TextColor=\"{AppThemeBinding Light=Green, Dark=Red}\"\n       BackgroundColor=\"{AppThemeBinding Light=White, Dark=Black}\" /&gt;\n\n&lt;!-- With resource references — preferred over literals --&gt;\n&lt;Label TextColor=\"{AppThemeBinding Light={StaticResource LightPrimary},\n                                   Dark={StaticResource DarkPrimary}}\" /&gt;\n</code></pre>\n<h3>C# Extension Methods</h3>\n<p>Show <strong>both</strong> when answering a \"light/dark colours in C#\" question — <code>SetAppThemeColor</code>\ncovers <code>Color</code> properties, <code>SetAppTheme&lt;T&gt;</code> covers everything else:</p>\n<pre><code>var label = new Label();\n\n// Color-specific helper\nlabel.SetAppThemeColor(Label.TextColorProperty, Colors.Green, Colors.Red);\n\n// Generic helper — works for any bindable property type, not just Color\nlabel.SetAppTheme&lt;Color&gt;(Label.TextColorProperty, Colors.Green, Colors.Red);\nlabel.SetAppTheme&lt;double&gt;(Label.FontSizeProperty, 14, 16);\n\n// The BindableProperty must belong to the object you call it on —\n// Image.SourceProperty goes on an Image, not a Label.\nvar image = new Image();\nimage.SetAppTheme&lt;ImageSource&gt;(Image.SourceProperty,\n    ImageSource.FromFile(\"logo_light.png\"),\n    ImageSource.FromFile(\"logo_dark.png\"));\n</code></pre>\n<p>Prefer defining the values as resource keys and referencing them, rather than\nscattering hardcoded colours across the codebase.</p>\n<h2>ResourceDictionary Theming (Custom Themes)</h2>\n<p>Use separate <code>ResourceDictionary</code> files with matching keys to define themes, then swap them at runtime.</p>\n<h3>Step 1 — Define Theme Dictionaries</h3>\n<p>When using compiled XAML with <code>x:Class</code> (as shown below), each dictionary needs a code-behind that calls <code>InitializeComponent()</code>. Dictionaries loaded via <code>Source</code> without <code>x:Class</code> do not need code-behind.</p>\n<p><strong>LightTheme.xaml</strong></p>\n<pre><code>&lt;ResourceDictionary xmlns=\"http://schemas.microsoft.com/dotnet/2021/maui\"\n                    xmlns:x=\"http://schemas.microsoft.com/winfx/2009/xaml\"\n                    x:Class=\"MyApp.Themes.LightTheme\"&gt;\n    &lt;Color x:Key=\"PageBackgroundColor\"&gt;White&lt;/Color&gt;\n    &lt;Color x:Key=\"PrimaryTextColor\"&gt;#333333&lt;/Color&gt;\n    &lt;Color x:Key=\"AccentColor\"&gt;#2196F3&lt;/Color&gt;\n&lt;/ResourceDictionary&gt;\n</code></pre>\n<p><strong>LightTheme.xaml.cs</strong></p>\n<pre><code>namespace MyApp.Themes;\n\npublic partial class LightTheme : ResourceDictionary\n{\n    public LightTheme() =&gt; InitializeComponent();\n}\n</code></pre>\n<p>Create a matching <strong>DarkTheme.xaml / DarkTheme.xaml.cs</strong> with the same keys and different values.</p>\n<h3>Step 2 — Consume with DynamicResource</h3>\n<p>Use <code>DynamicResource</code> so values update when the dictionary is swapped at runtime:</p>\n<pre><code>&lt;ContentPage BackgroundColor=\"{DynamicResource PageBackgroundColor}\"&gt;\n    &lt;Label Text=\"Hello\"\n           TextColor=\"{DynamicResource PrimaryTextColor}\" /&gt;\n    &lt;Button Text=\"Action\"\n            BackgroundColor=\"{DynamicResource AccentColor}\" /&gt;\n&lt;/ContentPage&gt;\n</code></pre>\n<h3>Step 3 — Switch Themes at Runtime</h3>\n<blockquote>\n<p>\uD83D\uDEA8 <strong>Never call <code>MergedDictionaries.Clear()</code> to swap a theme.</strong> The default MAUI\ntemplate merges <code>Resources/Styles/Colors.xaml</code> and <code>Styles.xaml</code> into\n<code>Application.Resources</code>. <code>Clear()</code> removes <strong>those too</strong>, so every implicit style,\nbrush and colour in the app silently disappears — buttons, entries and labels all\nrevert to unstyled defaults. Verified: after <code>Clear()</code>, <code>MergedDictionaries</code> drops\nfrom 2 to 1 and the template's <code>Primary</code> colour no longer resolves.</p>\n</blockquote>\n<p>Remove only the theme you added, and leave everything else alone:</p>\n<pre><code>static ResourceDictionary? _currentTheme;\n\nvoid ApplyTheme(ResourceDictionary theme)\n{\n    var merged = Application.Current!.Resources.MergedDictionaries;\n\n    // ✅ Remove ONLY the previous theme — Colors.xaml / Styles.xaml survive\n    if (_currentTheme is not null)\n        merged.Remove(_currentTheme);\n\n    merged.Add(theme);\n    _currentTheme = theme;\n}\n\n// Usage\nApplyTheme(new DarkTheme());\n</code></pre>\n<pre><code>// ❌ Destroys the app's Colors.xaml and Styles.xaml along with the old theme\nvar merged = Application.Current!.Resources.MergedDictionaries;\nmerged.Clear();\nmerged.Add(theme);\n</code></pre>\n<h2>System Theme Detection</h2>\n<h3>Read the Current Theme</h3>\n<pre><code>AppTheme currentTheme = Application.Current!.RequestedTheme;\n// Returns AppTheme.Light, AppTheme.Dark, or AppTheme.Unspecified\n</code></pre>\n<h3>Override the System Theme</h3>\n<pre><code>// Force dark mode regardless of OS setting\nApplication.Current!.UserAppTheme = AppTheme.Dark;\n\n// Reset to follow system theme\nApplication.Current!.UserAppTheme = AppTheme.Unspecified;\n</code></pre>\n<h3>React to Theme Changes</h3>\n<pre><code>Application.Current!.RequestedThemeChanged += (s, e) =&gt;\n{\n    AppTheme newTheme = e.RequestedTheme;\n    // Update UI or switch ResourceDictionaries\n};\n</code></pre>\n<h2>Combining Both Approaches</h2>\n<p>Use <code>AppThemeBinding</code> with <code>DynamicResource</code> values for maximum flexibility — the\nnested <code>DynamicResource</code> stays live, so swapping the dictionary updates the value\n<em>and</em> the OS light/dark switch is still honoured:</p>\n<pre><code>&lt;Label TextColor=\"{AppThemeBinding\n    Light={DynamicResource LightPrimary},\n    Dark={DynamicResource DarkPrimary}}\" /&gt;\n</code></pre>\n<p>Or react to system changes and swap full dictionaries:</p>\n<pre><code>Application.Current!.RequestedThemeChanged += (s, e) =&gt;\n{\n    ApplyTheme(e.RequestedTheme == AppTheme.Dark\n        ? new DarkTheme()\n        : new LightTheme());\n};\n</code></pre>\n<h2>Saving and Restoring User Preference</h2>\n<p>Store the user's choice with <code>Preferences</code> and apply it on startup:</p>\n<pre><code>// Save choice\nPreferences.Set(\"AppTheme\", \"Dark\");\n\n// Restore on startup (in App constructor or CreateWindow)\nvar saved = Preferences.Get(\"AppTheme\", \"System\");\nApplication.Current!.UserAppTheme = saved switch\n{\n    \"Light\" =&gt; AppTheme.Light,\n    \"Dark\"  =&gt; AppTheme.Dark,\n    _       =&gt; AppTheme.Unspecified\n};\n</code></pre>\n<h2>Common Pitfalls</h2>\n<h3>Android: ConfigChanges.UiMode is Required</h3>\n<p><code>MainActivity</code> <strong>must</strong> include <code>ConfigChanges.UiMode</code> or theme-change events will not fire and the activity restarts instead of handling the change gracefully:</p>\n<pre><code>[Activity(Theme = \"@style/Maui.SplashTheme\",\n          MainLauncher = true,\n          ConfigurationChanges = ConfigChanges.ScreenSize\n                               | ConfigChanges.Orientation\n                               | ConfigChanges.UiMode  // ← Required for theme detection\n                               | ConfigChanges.ScreenLayout\n                               | ConfigChanges.SmallestScreenSize\n                               | ConfigChanges.Density)]\npublic class MainActivity : MauiAppCompatActivity { }\n</code></pre>\n<p>Without <code>UiMode</code>, toggling dark mode in Android settings causes a full activity restart — losing navigation state and appearing as a crash. With it declared, the app stays alive and <code>RequestedThemeChanged</code> fires, so pair this fix with a handler that re-applies the theme (see below).</p>\n<h3>DynamicResource vs StaticResource</h3>\n<p>When using ResourceDictionary theme switching, you <strong>must</strong> use <code>DynamicResource</code>:</p>\n<pre><code>&lt;!-- ✅ Updates when theme dictionary changes --&gt;\n&lt;Label TextColor=\"{DynamicResource PrimaryTextColor}\" /&gt;\n\n&lt;!-- ❌ Frozen at first load — won't update on theme switch --&gt;\n&lt;Label TextColor=\"{StaticResource PrimaryTextColor}\" /&gt;\n</code></pre>\n<p><code>DynamicResource</code> only helps if something actually swaps the dictionary. When you\ndiagnose this, always show the swap and the system-theme hook alongside the fix —\notherwise the user has a corrected binding that still never updates:</p>\n<pre><code>static ResourceDictionary? _currentTheme;\n\nvoid ApplyTheme(bool useDark)\n{\n    var merged = Application.Current!.Resources.MergedDictionaries;\n\n    // Remove only the previous theme — never Clear(), which also wipes\n    // the template's Colors.xaml / Styles.xaml\n    if (_currentTheme is not null)\n        merged.Remove(_currentTheme);\n\n    _currentTheme = useDark ? new DarkTheme() : new LightTheme();\n    merged.Add(_currentTheme);\n}\n\n// React to the OS switching light/dark\nApplication.Current!.RequestedThemeChanged += (s, e) =&gt;\n    ApplyTheme(e.RequestedTheme == AppTheme.Dark);\n</code></pre>\n<h3>Hardcoded Colors Break Theming</h3>\n<p>Avoid inline color values on elements that should respect the theme:</p>\n<pre><code>&lt;!-- ❌ Will not change with theme --&gt;\n&lt;Label TextColor=\"#333333\" /&gt;\n\n&lt;!-- ✅ Theme-aware --&gt;\n&lt;Label TextColor=\"{DynamicResource PrimaryTextColor}\" /&gt;\n</code></pre>\n<h3>CSS Themes Cannot Be Swapped at Runtime</h3>\n<p>.NET MAUI supports CSS styling, but CSS-based themes <strong>cannot be swapped dynamically</strong>. Use ResourceDictionary theming for runtime switching.</p>\n<h3>Theme Keys Must Match Across Dictionaries</h3>\n<p>Every <code>x:Key</code> used in one theme dictionary must exist in all other theme dictionaries. A missing key causes a silent fallback to the default value, leading to inconsistent appearance.</p>\n<h2>Platform Support</h2>\n<table>\n<thead>\n<tr>\n<th>Platform</th>\n<th>Minimum Version</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>iOS</td>\n<td>13+</td>\n</tr>\n<tr>\n<td>Android</td>\n<td>10+ (API 29)</td>\n</tr>\n<tr>\n<td>macOS Catalyst</td>\n<td>10.15+</td>\n</tr>\n<tr>\n<td>Windows</td>\n<td>10+</td>\n</tr>\n</tbody>\n</table>\n<h2>Quick Reference</h2>\n<ul>\n<li><strong>OS light/dark</strong> → <code>AppThemeBinding</code> markup extension</li>\n<li><strong>Theme colors in C#</strong> → <code>SetAppThemeColor()</code>, <code>SetAppTheme&lt;T&gt;()</code></li>\n<li><strong>Read OS theme</strong> → <code>Application.Current.RequestedTheme</code></li>\n<li><strong>Force theme</strong> → <code>Application.Current.UserAppTheme = AppTheme.Dark</code></li>\n<li><strong>Theme changes</strong> → <code>RequestedThemeChanged</code> event</li>\n<li><strong>Custom switching</strong> → <code>Remove</code> the old theme from <code>MergedDictionaries</code>, then <code>Add</code> the new one — <strong>never <code>Clear()</code></strong></li>\n<li><strong>Runtime bindings</strong> → <strong><code>DynamicResource</code></strong> (not <code>StaticResource</code>)</li>\n<li><strong>Persist choice</strong> → <code>Preferences.Set</code> / <code>Preferences.Get</code></li>\n</ul>\n","files":[{"path":"references/theming-api.md","sizeBytes":4344,"isText":true},{"path":"SKILL.md","sizeBytes":16514,"isText":true}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"human-reviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"human-reviewed","screen":{"ran":true,"outcome":"flagged-cleared-by-moderator","suspicious":3,"notes":0,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-08-24T05:40:51.210451Z","sha256":"0875372349467721EB265E6C303BCF2C07FB757090FBEC0D8B8160DD2CAEF3D8","sizeBytes":7578},"review":null,"source":{"repositoryUrl":"https://github.com/dotnet/skills","path":"plugins/dotnet-maui/skills/maui-theming","license":"MIT","commit":"973cffbcdbae02557cc68ad0d41b8f20d60cfa03","subtreeSha":"59B1722E0FB3C3A923AEE7A0749D0650D02BE5B7156DA5D0A737B207FC3A6016","lastSyncedAt":"2026-10-01T15:23:51.767977Z"},"reviewedAt":"2026-08-27T16:52:34.34543Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/dotnet/skills/tree/main/plugins/dotnet-maui/skills/maui-theming"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install dotnet-skills@llmmart"},{"target":"git","command":"git clone https://github.com/dotnet/skills.git"}]}