A simple way to implement Dark Mode in .NET MAUI

Intro

I absolutely love dark app themes. As a person working a lot in front of computer screens and using mobile devices a lot, I have come to appreciate the ease on the eyes that Dark (or Night) Modes of software and mobile apps provide. In this blog post, I would like to show a simple way to implement this wonderful feature in order to provide a flawless user experience to your app's users without having to manipulate a ResourceDictionary at run-time.

As the convention goes these days, users should have the option to choose whether to use a Dark or Light Theme or to simply follow the system setting. .NET MAUI conveniently supports "Light" and "Dark" App Themes right out of the box. The sample Shell app even comes pre-configured with some styles that use the built-in markup extension AppThemeBinding. Let's explore how to take advantage of this. The code for this sample implementation can be found in my GitHub repository.

The UserAppTheme property and Styles

The Application class (App.xaml.cs) comes with a property of the enum type AppTheme called UserAppTheme which can be set to change the current theme:

// dark theme
UserAppTheme = AppTheme.Dark;

// light theme
UserAppTheme = AppTheme.Light;

// follow system default
UserAppTheme = AppTheme.Unspecified;

Coupled with the AppThemeBinding markup extension inside some Style elements in a ResourceDictionary (e.g. defined in the Styles.xaml file) this would already to suffice to set the theme once and be done:

<Style TargetType="ContentPage" ApplyToDerivedTypes="True">
    <Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
</Style>

Our app would now just be either shown with a black or white page background depending on the hard-coded value for the UserAppTheme property. This is already pretty neat, but we want to let the user decide. So let's do that.

Our own Theme class

In order to provide a meaningful user experience, we are going to implement our own Theme class which holds the AppTheme as well as a DisplayName.

The advantages of this class will become apparent further down. Hint: The DisplayName could be used in advanced scenarios to show a translated string depending on the selected language, but we won't cover localization of string resources in this article.

public sealed class Theme
{
    public static Theme Dark = new(AppTheme.Dark, "Night Mode");
    public static Theme Light = new(AppTheme.Light, "Day Mode");
    public static Theme System = new(AppTheme.Unspecified, "Follow System");

    public static List<Theme> AvailableThemes { get; } = new()
    {
        Dark,
        Light,
        System
    };

    public AppTheme AppTheme { get; }
    public string DisplayName { get; }

    private Theme(AppTheme theme, string displayName)
    {
        AppTheme = theme;
        DisplayName = displayName;
    }
}

Since the theme can usually be selected in a Settings section of an app, we will also implement a small SettingsService class.

The SettingsService

In this example, we simply create a small class that implements the INotifyPropertyChanged interface and use it as a singleton for convenience. I am skipping advanced topics like Dependency Injection and MVVM Code Generation.

public class SettingsService : INotifyPropertyChanged
{
    private static SettingsService _instance;
    public static SettingsService Instance => _instance ??= new SettingsService();

    private SettingsService()
    {
        // set the default (in advanced scenarios, this could be read from the preferences)
        Theme = Theme.System;
    }

    private Theme _theme;
    public Theme Theme
    {
        get => _theme;
        set
        {
           if(_theme == value) return;
            _theme = value;
           OnPropertyChanged();
        }
    }

    //...
}

We are going to use the SettingsService to hold the information about the current app theme, but it can also be extended to actually store the theme to the app's preferences. To see how this can be done, check out the repository with the sample code further down.

A sample UI

Now that we have the Theme and SettingsService classes, we can create a simple UI that will update its styling based on the selected AppTheme. We can add the following XAML to our MainPage (or any other Page for that matter) and bind to the SettingsService.Instance singleton and the DisplayName of our Theme class:

  <ContentPage.Resources>
    <ResourceDictionary>
      <Style TargetType="Label" x:Key="LabelStyle">
        <Setter Property="Text" Value="{AppThemeBinding Light=Light, Dark=Dark}" />
      </Style>
    </ResourceDictionary>
  </ContentPage.Resources>

  <Grid>
    <VerticalStackLayout
      Spacing="25"
      Padding="30,0"
      Margin="0,50,0,0"
      VerticalOptions="Start">

      <Label 
        HorizontalOptions="Center"
        Style="{DynamicResource LabelStyle}"
        FontSize="Title"/>

      <HorizontalStackLayout
        VerticalOptions="Center"
        HorizontalOptions="Center">
        <Label
          VerticalOptions="Center"
          Text="Select Theme: " />

        <!-- Note: We are binding to the SettingsService singleton and the DisplayName of the Theme -->
        <Picker
          VerticalOptions="Center"
          ItemsSource="{Binding Source={x:Static models:Theme.AvailableThemes}}"
          ItemDisplayBinding="{Binding DisplayName}"
          SelectedItem="{Binding Path=Theme, Source={x:Static services:SettingsService.Instance}}"/>
      </HorizontalStackLayout>

    </VerticalStackLayout>
  </Grid>

We also need to define a few more styles in our main ResourceDictionary which lives in the Styles.xaml file of our project, but they can also be defined anywhere else:

  <Style TargetType="ContentPage" ApplyToDerivedTypes="True">
    <Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
  </Style>

  <Style TargetType="Label">
    <Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray500}, Dark={StaticResource White}}" />
  </Style>

  <Style TargetType="Picker">
    <Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray500}, Dark={StaticResource White}}" />
  </Style>

This should roughly look like this:

00_SampleUI.PNG

At the moment, nothing will actually happen when we tap on the Picker and select any of the Themes, because we still need to wire up the SettingService with the App's UserAppTheme property. Let's do that next.

Bringing it all together

The final step is to subscribe to the PropertyChanged event of our SettingsService in order to be able to react to changes in the settings. We will do this from within the App.xaml.cs, because that's where the UserAppTheme property lives:

public App()
{
    InitializeComponent();
    MainPage = new AppShell();

    // let's set the initial theme already during the app start
    SetTheme();

    // subscribe to changes in the settings
    SettingsService.Instance.PropertyChanged += OnSettingsPropertyChanged;
}

private void OnSettingsPropertyChanged(object sender, PropertyChangedEventArgs e)
{
    if (e.PropertyName == nameof(SettingsService.Theme))
    {
        SetTheme();
    }
}

private void SetTheme()
{
    UserAppTheme = SettingsService.Instance?.Theme != null
                 ? SettingsService.Instance.Theme.AppTheme
                 : AppTheme.Unspecified;
}

Essentially, what we are doing here is to respond to changes in the settings directly. This is possible, because our SettingsService implements the INotifyPropertyChanged interface.

Running the app

Finally, we can run the app and select the App Theme during run-time, which is pretty awesome:

01_SelectTheme.PNG

02_DarkMode.PNG

Conclusion and further writing

.NET MAUI comes with a lot of functionality out of the box and ready to use. I hope this blog post gives you an idea of how to provide an easy way to add a Dark Mode to your app and let your users select their preferred App Theme.

In future posts, I will show more advanced topics like wiring up the Picker control with localized strings, creating custom components and sharing them via nuget.org, as well as using MVVM Code Generation to your advantage to reduce. Stay tuned!

If you enjoyed this blog post, you may want to follow me on LinkedIn, subscribe to this blog and star the GitHub repository for this post.

Note: At the time of writing, I have used Visual Studio 2022 17.4 Preview 2.1 and .NET 7 RC1.