User preferences store including “Dark Mode” or custom themes in a Blazor Wasm app

The first step in creating the ability for users to record preferences such as dark mode or custom themes or any other client side preferences is to create a store, Lets create PreferencesStateContainer.cs

public class PreferencesStateContainer
{
    public bool IsDarkMode { get; set; } = false;
    public readonly string DarkModeStorageKey = "DarkMode";

    public event Action OnStateChange;

    private void NotifyStateChanged() => OnStateChange?.Invoke();

    public async Task<bool> GetUserPreferenceDarkMode(IJSRuntime JS)
    {
        var _dark = await JS.InvokeAsync<string?>("localStorage.getItem", DarkModeStorageKey);

        if (_dark is null)
        {
            SetUserPreferenceDarkMode(JS, false);
            return false;
        }

        IsDarkMode = _dark == "True";
        NotifyStateChanged();
        return _dark == "True";
    }

    public async void ToggleDarkMode(IJSRuntime JS)
    {
        SetUserPreferenceDarkMode(JS, !IsDarkMode);
    }

    public async void SetUserPreferenceDarkMode(IJSRunetime JS, bool value)
    {
        await JS.InvokeVoidAsync("localStorage.setItem", DarkModeStorageKey, value.ToString());
        IsDarkMode = value;
        NotifyStateChanged();
    }
}

Now that we have a state container with the ability to ToggleDarkMode we need to register this in our service container, eg in Program.cs

builder.Services.AddScoped<PreferencesStateContainer>();

Then wherever we set up our theme we can inject the preferences container and use our IsDarkMode to determine which theme to load, in my app this is using MudBlazor, but the same approach would work for any other UI library.

Providers.razor

@inject PreferencesStateContainer UserPreferences
@inect IJSRuntime JS

<MudThemeProvider @bind-IsDarkMode="@UserPreferences.IsDarkMode" Theme="@UserPreferences.GetTheme()" />

MainLayout.razor

@inject IJSRuntime JS
@inject PreferencesStateContainer UserPreferences

<Providers />
<MudLayout
    data-dark-mode="@(UserPreferences.IsDarkMode ? "dark" : "light")"
>
    ...
    <MudToggleIconButton
        Toggled="UserPreferences.IsDarkMode"
        ToggledIcon="@Icons.Material.Filled.DarkMode"
        Color="@Color.Inherit"
        Icon="@Icons.Material.Filled.LightMode"
        ToggledColor="@Color.Inherit"
        title="@(UserPreferences.IsDarkMode ? "Activate Light Mode" : "Activate Dark Mode")"
    />
</MudLayout>

@code {
    public async Task ToggleDark()
    {
        UserPreferences.ToggleDarkMode(JS);
    }
}

Above we inject our preferences state container and the JS runtime into our main layout, and use the state container in conjunction with an icon button to toggle our dark mode.