Dark Theme Switcher with Bootstrap 5.3

 


A few years ago I posted an article on how to make a CSS theme switcher using CSS and I had a post on how to switch modes when using TailWinds.  With Bootstrap 5.3 there is a quick and easy way to change the application theme from light to dark and vice versa. You can still use the CSS method and it does give you more flexibility, but you have to do all the CSS styling yourself.


In the CSS model, you would dynamically change the overall style sheet.  While with the Tailwinds solution, you had to set a dark attribute on the elements to control the theme. In Bootstrap 5.3 and beyond, you just use a new data attribute on the body element in App.razor:


            <body data-bs-theme:"dark">


With Bootstrap, you have to use JavaScript to do the actual switching of the data attribute. These are the parts you have to implement to have just switch the dark mode on and off:

    1. Service to handle the state if dark mode is on or off and to call the Javascript
    2. You need the Javascript code in App.razor.
    3. Add the "data-bs-theme: dark" to the body tag, as above
    4. Add a button and click event to execute the actual mode switch.
    5. Add the Service to the builder in Program.cs


Service

    public class ThemeService
    {
        private readonly IJSRuntime? js;
        private bool isDarkTheme = false;

            public bool IsDarkTheme 
            { 
                get => isDarkTheme;
                set {
                    isDarkTheme = value;
                    // Set the theme in the browser
                    js.InvokeVoidAsync("setDarkTheme", value);
                }
            }

            public ThemeService(IJSRuntime js)
            {
                this.js = js;
            }
    }

The key part here is injecting the JSRuntime and the properties to hold the dark mode state.


JavaScript

        <script>
            //Set the theme
            window.setDarkTheme = (isDarkMode) => {
                var el = document.querySelector('body');
                var data = el.getAttribute('data-bs-theme');
                if (isDarkMode) {
                    //Set theme
                    el.setAttribute('data-bs-theme', "dark");
                }
                else {
                    //remove theme
                    el.removeAttribute('data-bs-theme');
                }
            }
        </script>

This script is different than the one used in the CSS post.  Here we have to add/remove the data-bs-theme attribute from the body element.  Notice it is not a class but a data attribute.


Button

I created a simple button on the home page to trigger the changes between modes:

            <button class="btn btn-primary" @onclick="ToggleTheme">Toggle Theme</button>

With the Handler of:

           private void ToggleTheme()
           {
               //If the theme is dark, set it to light and vice versa
               service.IsDarkTheme = !service.IsDarkTheme;        
           }

That is all it takes to have a manual dark mode switcher.


More

There is more we need to be concerned with at this point:

1. On startup we should default to what the browser/system OS has set for Dark mode.
2. When the user changes the browser dark mode settings, the application should change as well without having to restart.
3. If the user does change the mode manually, we should save it as a user preference.

I have a solution for 1 and 2.  #3, which I will cover in a later post.


Default On start-up


We need more Javascript, a new method in our service, and some code on the home page.

New Javascript

            const prefersDark = "(perfers-color-scheme: dark)"

            window.isBrowerDarkTheme = () => window.matchMedia('(prefers-color-scheme: dark)').matches;

This code checks the user browser preferences and returns true if dark mode is the default.

New Service Method

            public async Task<bool> IsThemeDark() 
            {
                // Check if the browser is in dark mode
                var result = await js.InvokeAsync<bool>("isBrowerDarkTheme");
                return result;
             }

This code calls the javascript to get the default.

Home Page


        protected override async Task OnAfterRenderAsync(bool firstRender)
        {
            if (firstRender)
            {    
                //Set the theme
                service.IsDarkTheme = await service.IsThemeDark();
            }
        }

This code will go and check the browser default mode and then set the state of the dark mode variable to that correct value.  Notice will only do this on the first Render.

That is all it takes to use the default value for the dark mode.

Dynamically change when the Browser Settings change

Even though this is a rare occurrence, when was the last time you changed your browser settings, a well-behaved application should support it.  And it is not hard to implement. Again we have changes in the JavaScript, the Service, and the Home page.  We need an event listener and a method to handle the event.


New Javascript

            window.addListerForThemeChanged = (ThemeObject) => {
                window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
                    ThemeObject.invokeMethodAsync('SetDarkTheme', e.matches);
                 });
            }

This code adds the event listener to the preferred color scheme. and will call the service method SetDarkTheme.  Yes, this has Javascript calling into the C# code.

New Service Method

        public event Action? OnChange;

        public async Task ListenForThemeChanged()
        {
            // Listen for theme changes in the browser
            var ThemeObject = DotNetObjectReference.Create(this);
            await js.InvokeVoidAsync("addListerForThemeChanged", ThemeObject);
        }

        [JSInvokable]
        public async Task SetDarkTheme(bool isDark) => IsDarkTheme = isDark;

This code creates an event for the Home page, creates the handler for the JavaScript to call, and the handler to change the dark mode value.

Home Page


        protected override void OnInitialized()
        {
            //If the theme changed, re-render the component
            service.OnChange += StateHasChanged;
        }

Add a handler to update the UI on change.

We also need to add:

             await service.ListenForThemeChanged();

To the OnAfterREnderAsync method.  This configures the right event handler once the browser triggers the change.


Summary

This turned out to be coding than I had originally thought it would be.  I like the plain CSS best between the 3 ways I have added dark mode themes.  It is more work, but it gives you the most flexibility.

It should be noted that the solution to use the browser dark mode setting did not work in Vivaldi.  Chrome and Edge seemed to work fine.  That might be something to take into consideration.




Comments

Popular posts from this blog

Yes, Blazor Server can scale!

Blazor new and improved Search Box

Blazor Wizard Step Component