Dark Mode With Blazor and Tailwind Css

Rasheed K Mozaffar - Sep 22 '23 - - Dev Community

Introduction

Have you ever wondered how you can add a switch between light theme and dark theme? Most modern websites have support for both themes as the vast majority of people prefer dark mode as it's less tiresome on the eyes, and you don't want to burn your end user's eyes when they are in a dark room, with your website shoving tons of bright white light straight up their eyes, so please, let me show you how you can add, the mighty Dark Mode to your applications.

Requirements

1: A Blazor WebAssembly Empty project, also Blazor Server works, but the reason I'm going with the empty template is that Blazor's normal template comes equipped with Bootstrap, and a lot of boilerplate which we're definitely not interested in for our purpose.

2: Tailwind Css. If you don't know how to add Tailwind Css to your Blazor projects, then kindly reference my article on how you can set it up as we will be using Tailwind in this guide
https://dev.to/rasheedmozaffar/using-tailwind-css-with-net-blazor-4ng7

3: We'll also be using the browser's local storage to persist the theme our user chooses, so in case you don't know how to interact with it, I have a full article that can guide you through the setup, and will also teach you everything you need to know about dealing with the local storage from your Blazor applications
https://dev.to/rasheedmozaffar/how-to-work-with-the-browsers-local-storage-in-blazor-58kc

💡 Note: I will not go over the installation and setup processes of the local storage package and tailwind through the guide, so if you want to follow along, please reference the articles I've linked before so that you can get your Blazor project prepped up for what we'll be working with during this guide.

Configuring Tailwind's Dark Mode Preference

By default, Tailwind Css uses a CSS media feature that is prefers-color-scheme, and every style made for dark mode, will appear when the user changes their system preferences over to dark theme. That's cool, but we want something better, we want a Toggle, A toggle that lets its clickers explore the two sides of the website, a tap that transports them from one realm to another, from the bright to the dark, from the serene to the intense. Experience the contrast and the harmony, the light and the shadow, the yin and the yang. This is not just a switch, this is an adventure.
Ok that was dramatic enough, let's move on to the configuration file.

  • Open tailwind.config.js (In the root directory of the project)
  • Add a new property called darkMode and set it to class

The file should look like this:




/** @type {import('tailwindcss').Config} */
module.exports = {
  darkMode: 'class',
  content: ['./**/*.{razor,html}'],
  theme: {
    extend: {},
  },
  plugins: [],
}



Enter fullscreen mode Exit fullscreen mode

Tailwind offers a class strategy as an alternative to the media thing we've talked about before, when the dark class is attached to the html parent container, all children elements with dark styles will come into life, like this:




<main class="dark">
    <h1 class="text-3xl dark:text-white font-bold text-slate-600">Hello, World!</h1>
</main>



Enter fullscreen mode Exit fullscreen mode

Since the dark class is present, the h1 will be white, and so every other dark style that we apply inside the main element.

The Approach

In Blazor, we have the MainLayout, which is most likely going to be used across all the pages, inside this layout, we will have this:




<main>
    @Body
</main>



Enter fullscreen mode Exit fullscreen mode

This main element, will wrap all of our pages' content, and thus making it the parent for all the items in a page, so what we will be doing is, we'll store our theme as a key in the local storage, and use conditionals to determine which theme to use, if the theme to use is dark, we'll attach the dark class to the parent, otherwise we'll just use null implying that we want to use light mode which is the default.

Coding The Implementation

Inside MainLayout, add the following code inside the code directory:




@code {
    private bool toggleDark = false;

    protected override async Task OnInitializedAsync()
    {
        if (!(await LocalStorage.ContainKeyAsync("theme")))
        {
            await LocalStorage.SetItemAsStringAsync("theme", "light");
        }
        else
        {
            string theme = await LocalStorage.GetItemAsStringAsync("theme");

            if (theme == "dark")
            {
                toggleDark = true;
            }
            else
            {
                toggleDark = false;
            }
        }
    }
}



Enter fullscreen mode Exit fullscreen mode

Ok what's going on?

  • We're creating a boolean variable called toggleDark, this variable will determine whether we want to toggle dark mode on or not.
  • We're overriding OnInitializedAsync to do two things:

    • When the app is used for the first time on a browser, there'll be no theme key in the local storage, so we're checking to see if such a key is there, if not, add it and give the value light, this value is honestly meaningless, it's just there because using null as a value for the key results in an error
    • If the theme key has a value, grab it, and check if it's set to dark, then change the value of toggleDark to true therefore enabling dark mode. However, if the value is something else, light in this case, then flip toggleDark to false, and that means light mode is the theme of choice.

With that done, we now want to make use of toggleDark to display the appropriate theme, to do that, we need to add a conditional class to the main element wrapping the @Body directive, we can do that as follows:




<main class='@(toggleDark ? "dark" : null)'>
    @Body
</main>



Enter fullscreen mode Exit fullscreen mode

Here, we're using the conditional operator aka ternary operator aka inline if operator, whoa that dude got names!
When the value of toggleDark is set to true, then we want to attach the dark class, however if toggleDark is false, then just don't add any class to the element.

Now we're almost done, what's left for us to do, is have a switch that will let us control which theme to use, because now if we want to switch modes, we'd have to do that from the local storage by changing the value of the theme key by hand to dark, and of course that makes no sense.

Adding The Toggle Button

Now we'll design some basic button and set an @onclick event on it, when the event fires, the toggleDark gets changed to the opposite of its current value, and the new theme is changed inside the local storage, to keep the state in sync with our theme of choice.

Inside MainLayout, inside main, add a header element, I don't think it's the right way to add the header inside of main, I think a better practice would be adding a div wrapping both the header and main, and on that div attach the conditional operator which would determine the theme, but for now, we'll do it like that.
Add this code in the header to create a basic navigation bar with a toggle:




<header class="p-4 flex justify-between ... ">
    <div>
        <h1 class="text-xl font-bold text-slate-700">Theming Demo</h1>
    </div>

    <div>
        <button class="rounded-md bg-blue-100 px-4 py-2 text-sm font-medium text-blue-900">
            Toggle Dark/Light Mode
        </button>
    </div>
</header>



Enter fullscreen mode Exit fullscreen mode

Don't bother too much if this looks confusing, it's just for aesthetic purposes and has nothing to do with the actual implementation.

Writing The Change Theme Logic

Below OnInitializedAsync, we'll create a new method called ChangeTheme, the content of which is as follows:




private async Task ChangeTheme()
{
    toggleDark = !toggleDark;
    string theme = await LocalStorage.GetItemAsStringAsync("theme");

    if (theme == "dark")
    {
        await LocalStorage.SetItemAsStringAsync("theme", "light");
    }
    else
    {
        await LocalStorage.SetItemAsStringAsync("theme", "dark");
    }
}



Enter fullscreen mode Exit fullscreen mode

Let's explain it:

  • We begin by inverting the value of toggleDark, so if's set to true, after this assignment, it'll become false and vice versa
  • After that, we grab the value of the key theme from the local storage of the browser
    • If theme is dark, we want to flip it to light
    • If it's something else, we want to set it to dark

Now we have to hook this method so that it gets called when the button aka toggle is clicked, for that, simply add this to the button we created earlier:




<button @onclick="ChangeTheme"
class="rounded-md ...">
    Toggle Dark/Light Mode
</button>



Enter fullscreen mode Exit fullscreen mode

And There You Have It 🌟

All you have to do right now, is when designing your components, you just add the dark styles to them, when in light mode, those styles won't be visible, but when you click that toggle, and switch to dark mode, your components will now use their dark styles.

Results

If you go to Index, and add the following markup:




<div class="h-[100vh] p-4 w-full bg-violet-200 dark:bg-slate-800">
    <h1 class="text-3xl text-blue-500 dark:text-white font-bold">Hello, World!</h1>
</div>



Enter fullscreen mode Exit fullscreen mode

You run the project, and click on the toggle, you should see the background and text colors changing, so your output should be something similar to this

Light Theme

Light theme result

Dark Theme

Dark theme result

Conclusion

In this post, you've learned how we can get dark mode to work along side light mode, we also saw how we can persist the theme the user chooses in the local storage so that if the user closes your application and re open it later, the theme they were using the last time they visited your site would be used again, making your app more flexible and more modern with the help of toggling between themes.
If you have any questions, suggestions, or something you would like to point out, please add it in the comments down below.

Thanks for reading along!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .