Click here to Skip to main content
16,018,818 members
Articles / Web Development / Blazor

Blazor Web Assembly (WASM) Theme Switching

Rate me:
Please Sign up or sign in to vote.
5.00/5 (5 votes)
22 Jan 2022CPOL7 min read 21K   374   13   10
User preference theme support - supporting OS / Browser preference to custom user selection
This article will cover the design process, inspiration, and implementation using modern CSS techniques.

Contents

Introduction

This article discusses from need to implementation to give an understanding of what and how the theme support is implemented and used. Snippets of code are used to help with the article, not a complete code dump. All code and sample projects are included in the download for further study and trying out.

The article assumes that you have a basic understanding of Blazor, and links to various external resources to where you may require further information and/or explanation.

Inspiration

I wanted a theme switching button that would be modern with animation. I liked the button on Google Fonts website but I am a back-end developer, not a UI web designer. Here is their button in action.

Image 1

(click on the image above to see how the theme switching works)

Luckily, Kevin Powell accepted a challenge from one of his viewers and created an equivalent button. You can view how it made it on his YouTube channel.

Below is Kevin's button integrated into this Blazor solution.

Image 2

(click on the image above to see how the theme switching works)

Design

The concept is:

  1. Minimal code - fully wrapped or open to custom implementation
  2. Open design to work with any bespoke code or CSS framework like Bootstrap, Tailwind, etc.
  3. Reusable across multiple projects
  4. Minimal JavaScript if unavoidable
  5. Latest Blazor and CSS3 coding techniques

Implementation

For this article, two different theme switch methods are used:

1. Swapping Stylesheets

Allowing distinct separation of themes into separate files. This allows for downloading themes from 3rd party websites like Bootswatch. I've used their Darkly (dark theme) and Flatly (light theme) for this article. This is an older theme switching technique.

The best place to implement theme support is close to the top of the DOM as possible. This is done in the MainLayout.razor.

For stylesheet switching, we need to alter the page head section. To do this, we use the HeadContent component in the MainLayout.razor:

HTML
<HeadContent>
    /* elements go here */
</HeadContent>

When using the HeadContent component, the order is important. The component add content to the bottom of the page head.

The CSS stylesheet(s) are normally added to theindex.html file. However, for switching stylesheets, we remove the color styling from the index.html  and place the files in the <HeadContent> block in the MainLayout.razorfile:

HTML
<HeadContent>
    <Themes>
        <DarkMode>
            <link href="css/bootstrap/darkly.min.css" rel="stylesheet" />
        </DarkMode>
        <LightMode>
            <link href="css/bootstrap/flatly.min.css" rel="stylesheet" />
        </LightMode>
    </Themes>
    <link href="css/app.css" rel="stylesheet" />
    <link href="ThemeByStylesheetDemo.styles.css" rel="stylesheet" />
</HeadContent>

No changes are required to the app CSS as the same CSS rules are applied in each theme file. This method is the least invasive however when switching themes there may be a slight flicker to the page when the browser refreshes.

2. CSS Variables

CSS variables, also called Custom Properties, is the modern and recommended technique used in websites today. CSS frameworks like Open Props use CSS Variables extensively.

Theme switching is done using a CSS class. In our case, we default to light mode, and add a CSS class name like dark to switch modes. The CSS markup would look something like:

CSS
:root {
    /* Light theme */
    --background: #fff;
    --font-color: #000;
    --font-color-2: #fff;
    --highlight: #f7f7f7;
    --highlight-2: #95a6a6;
    --link: #0366d6;
}

.dark {
    /* dark theme */
    --background: #222;
    --font-color: #fff;
    --font-color-2: #fff;
    --highlight: #393939;
    --highlight-2: #444444;
    --link: #3ca4ff;
}

To use CSS variables:

CSS
.page {
    background-color: var(--background);
    color: var(--font-color);
}

This method does require the use of CSS variables, so changes to existing stylesheet code and inline style rules is required however the benefit is that there is no flickering when the browser updates. The other benefit is that you are now using shared variables and your css is easier to maintain.

3. Theme Switching

The animated samples above, like for the Google Fonts website, have a button for manual switching. There is also a media query for detecting user changes via the OS or web browser.

So typically in CSS, we would use the prefers-color-scheme media query.

For this to work, we need to listen for the change event. Blazor currently cannot directly listen to media queries, so we need to use some JavaScript with a callback into Blazor.

Here is the JavaScript:

JavaScript
function createThemeListener(dotNetRef) {
    window.matchMedia("(prefers-color-scheme: dark)").addListener(
        e => dotNetRef.invokeMethodAsync("DarkModeStateChanged", e.matches)
    );

The initializer in Blazor where we pass a reference to our class with the callback:

C#
_jsRuntime = jsRuntime;
_moduleTask = new(() => jsRuntime.ModuleFactory(ScriptFile));

IJSObjectReference module = await _moduleTask!.Value;
DotNetInstance = DotNetObjectReference.Create(this);

And the callback for the JavaScript event to Blazor:

C#
[JSInvokable]
public async Task DarkModeStateChanged(bool state)
    => await SetDarkModeAsync(state).ConfigureAwait(false);

The javascript code lives in the library. With the latest version of Blazor, it is possible to include the javascript file without the need to manually add it to the index.html file. To do this, we export the javascript functions. The compiler sees this and includes the javascript for us. So the updated javascript looks like this:

JavaScript
export function isDarkTheme() {
    return  window.matchMedia("(prefers-color-scheme: dark)").matches;
}

export function createThemeListener(dotNetRef) {
    window.matchMedia("(prefers-color-scheme: dark)").addListener(
        e => dotNetRef.invokeMethodAsync("DarkModeStateChanged", e.matches)
    );
}

export function getLocalStorage(key) {
    return localStorage[key];
}
export function setLocalStorage(key, value) {
    localStorage[key] = value;
}

You can read more about how this works in Microsoft's Documentation.

4. Linking the Button to the Switching

There are three parts to enabling theme switching:

  1. User Selection - In this case, a toggle button. You could also use a dropdown list or a more bespoke selection.
  2. Switching the theme in MainLayout.razor file.
  3. Linking the selection to the switching. We will use a service called ThemeService for this.

Dot Net Core uses IOC container to implement Dependency Injection to automagically wire up classes with their dependencies.

The ThemeService class handles the shared theme state and notification of changes from the user either via the ThemeToggle button component or via OS or browser changes. Any changes made are handled in the MainLayout.razor component.

The Code

Sample projects are included to demonstrate how each theme switching mode works. Both sample projects use the included ThemeToggle component, however you can switch it out with your own.

Theme Library

The library encapsulates all core functionality for easy reuse:

  • auto inclusion of all library CSS & JavaScript in the main project

1. ThemeToggle Component

  • Aria compliant
  • BEM CSS class naming convention
  • Animated with minimal animation used
  • Light or Dark state
  • Optionally ShowTooltip property
  • Custom DarkTipMessage & LightTipMessage properties
  • Supports 16, 24, 43, 48 pixel ButtonSize
  • Custom Style, class and attribute
  • OnDarkModeStateChanged event
HTML
@inject IThemeService themeService

<button @attributes="@Attributes"
        style="@Style"
        class="@GetComponentCssClass()"
        aria-label="@GetToolTip()"
        @onclick="_ => ToggleTheme()">
    <svg xmlns="http://www.w3.org/2000/svg"
         max-width="24px" max-height="24px"
         viewBox="0 0 472.39 472.39">
        <g class="theme-toggle__sun">
            <path d="M403.21,167V69.18H305.38L236.2,0,167,69.18H69.18V167L0,236.2l69.18,
             69.18v97.83H167l69.18,69.18,69.18-69.18h97.83V305.38l69.18-69.18Zm-167,
             198.17a129,129,0,1,1,129-129A129,129,0,0,1,236.2,365.19Z"/>
        </g>
        <g class="theme-toggle__circle">
            <circle cx="236.2" cy="236.2" r="103.78"/>
        </g>
    </svg>
</button>

When the button is pressed, it notifies the ThemeService:

C#
private void ToggleTheme()
    => themeService.DarkMode = !themeService.DarkMode;

2. ThemeService (core)

  • DarkMode property for the theme state - light or dark

  • Store state changes to the browser's localstorage to remember user selection for page reloads, changes and later website revisits.

  • Listens for the prefers-color-scheme media query changed event

  • OnDarkModeStateChanged event for notifying changes

When a change is made to the state, the following code is executed:

C#
private async Task SetDarkModeAsync(bool value)
{
    _darkMode = value;

    // store user's currently selected color scheme for the app
    await (await GetModuleInstance())
        .SetLocalStorageThemeAsync(_darkMode);

    OnDarkModeStateChanged?.Invoke(DarkMode);
}

SetLocalStorageThemeAsync is an extension method that wraps the JavaScript call for storage:

C#
internal static class IJSObjectReferenceExtensions
{
    private static string JSSetLocalStorage = "setLocalStorage"; 

    public static async Task SetLocalStorageAsync(
        this IJSObjectReference? jsObjRef, string key, string value)
        => await jsObjRef!.InvokeVoidAsync(JSSetLocalStorage, key, value)
                          .ConfigureAwait(false);

    public static async Task SetLocalStorageThemeAsync(
        this IJSObjectReference? jsObjRef, bool IsDarkTheme)
        => await jsObjRef!.SetLocalStorageAsync(ThemeKey,IsDarkTheme
            ? DarkThemeValue : LightThemeValue).ConfigureAwait(false);
}

and the JavaScript:

JavaScript
function setLocalStorage(key, value) {
    localStorage[key] = value;
}

3a. Themes Component Method

This component is used for Stylesheet Switching:

  • Auto-selection of Light or Dark theme selection
  • Initializes the ThemeService and listens to the OnDarkModeStateChanged event for changes and raises a render update.

This is the theme switching markup:

HTML
@if (ThemeService is not null && ThemeService.DarkMode)
{
    @if (DarkMode is not null)
    {
        @DarkMode
    }
}
else
{
    @if (LightMode is not null)
    {
        @LightMode
    }
}

And the code that listens and raises a render update:

C#
protected override async Task OnInitializedAsync()
{
    if (ThemeService is not null)
    {
        await ThemeService.InitializeAsync()!;
        ThemeService.OnDarkModeStateChanged+= OnDarkModeChanged;
    }

    await base.OnInitializedAsync();
}

private void OnDarkModeChanged(bool State) => StateHasChanged();

In your app, the component usage in MainLayout.razor would be:

HTML
<HeadContent>
    <Theming.Themes>
        <DarkMode>
            <link href="css/bootstrap/darkly.min.css" rel="stylesheet" />
        </DarkMode>
        <LightMode>
            <link href="css/bootstrap/flatly.min.css" rel="stylesheet" />
        </LightMode>
    </Theming.Themes>
    <link href="css/app.css" rel="stylesheet" />
    <link href="ThemeTest.styles.css" rel="stylesheet" />
</HeadContent>

3b. CSS Class Change Method

Manual wiring up the MainLayout.razor for CSS class selection:

HTML
@inject IThemeService themeService

<div class="@GetClassCss()">

and the code to manage the classes:

C#
private bool IsDarkMode;

protected override async void OnInitialized()
{
    // uncomment if not using our ThemeToggle component
    //await themeService.InitializeAsync();
    themeService.OnDarkModeStateChanged += OnDarkModeChanged;
    await base.OnInitializedAsync();
}

private void OnDarkModeChanged(bool state)
{
    IsDarkMode = state;
    StateHasChanged();
}

private string GetClassCss()
    => "page" + (IsDarkMode ? " dark" : "");

Testing Theme Switching

To test switching between light and dark themes, you can either set your preferences in Windows or Mac OS or use the settings / developer tools in web browsers. Below, I've listed where to find the options in common browsers.

Chrome/Edge

  1. Open the developer tools
  2. Click on the 3 dots for more options, then More tools, and select "Rendering".

    Image 3

  3. Scroll down until you reach the "prefers-color-scheme" dropdown selection.

    Image 4

FireFox

Open the developer tools, Page Inspector, and there are buttons to toggle between light (sun) and dark (moon) modes.

Image 5

Opera

Select the "Easy Setup" button on the far right and you can choose between light, dark, and system OS modes.

Image 6

Summary

The library encapsulates all functionality required to manage theme state and switching with automatic storage, supports multiple theming techniques, and a modern Toggle. Only a handful of lines of code are required to implement into your own projects.

Enjoy!

History

  • v1.0 - 23rd January, 2022 - Initial release
  • v1.01 - 31st January, 2022 - added more information to the Implementation section.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Technical Lead
Australia Australia
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionDifficulty Implementing Pin
Member 1577372522-Sep-22 9:18
Member 1577372522-Sep-22 9:18 
AnswerRe: Difficulty Implementing Pin
Graeme_Grant22-Sep-22 17:12
mvaGraeme_Grant22-Sep-22 17:12 
GeneralRe: Difficulty Implementing Pin
Member 1577372525-Sep-22 17:06
Member 1577372525-Sep-22 17:06 
GeneralRe: Difficulty Implementing Pin
Graeme_Grant25-Sep-22 18:36
mvaGraeme_Grant25-Sep-22 18:36 
GeneralRe: Difficulty Implementing Pin
Member 1577372526-Sep-22 6:20
Member 1577372526-Sep-22 6:20 
GeneralRe: Difficulty Implementing Pin
Graeme_Grant26-Sep-22 13:52
mvaGraeme_Grant26-Sep-22 13:52 
GeneralRe: Difficulty Implementing Pin
Member 157737256-Oct-22 6:06
Member 157737256-Oct-22 6:06 
GeneralRe: Difficulty Implementing Pin
Graeme_Grant6-Oct-22 9:36
mvaGraeme_Grant6-Oct-22 9:36 
GeneralMy vote of 5 Pin
Ștefan-Mihai MOGA23-Jan-22 20:25
professionalȘtefan-Mihai MOGA23-Jan-22 20:25 
GeneralRe: My vote of 5 Pin
Graeme_Grant23-Jan-22 23:27
mvaGraeme_Grant23-Jan-22 23:27 
GeneralMessage Closed Pin
23-Jan-22 1:36
Mark William 202223-Jan-22 1:36 
PraiseMessage Closed Pin
22-Jan-22 14:17
Kacper Mxm22-Jan-22 14:17 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.