Tailwind Class Based Theme Changer for Blazor Apps

This post assumes that you know how to add Tailwind CSS in a Blazor Application. Read this post if you don't already!

In this notebook, we'll explore the implementation of a class-based theme changer for Blazor applications using Tailwind CSS. This approach allows us to seamlessly switch between light and dark themes while leveraging the power and simplicity of Tailwind CSS for styling.

Tailwind CSS is a popular utility-first CSS framework that enables rapid UI development by providing a set of pre-defined classes. Blazor, on the other hand, is a framework for building interactive web UIs using C# instead of JavaScript. By combining Tailwind CSS with Blazor, we can create modern and responsive web applications efficiently.

One common feature in web applications is the ability to switch between different themes, such as light and dark themes. In this notebook, we'll demonstrate how to implement a theme changer using Tailwind CSS classes in a Blazor application.

Create a Blazor Server App. Create a razor component in a new folder named widget. This is the component that toggles between light and dark theme.

@inject IJSRuntime Js
@inject ProtectedLocalStorage ProtectedLocalStorage
@inject AppState AppState

<button class="text-gray-500 hover:bg-bg-gray-800 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg text-sm p-2.5 inline-flex items-center"
        @onclick="ToggleTheme">
    @if (AppState.Theme == "dark")
    {
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-moon"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" /></svg>
    }
    else
    {
        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-sun"><circle cx="12" cy="12" r="4" /><path d="M12 2v2" /><path d="M12 20v2" /><path d="m4.93 4.93 1.41 1.41" /><path d="m17.66 17.66 1.41 1.41" /><path d="M2 12h2" /><path d="M20 12h2" /><path d="m6.34 17.66-1.41 1.41" /><path d="m19.07 4.93-1.41 1.41" /></svg>
    }
</button>

@code {
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await SetTheme();
        }
    }

    private async Task SetTheme()
    {
        var result = await ProtectedLocalStorage.GetAsync<string>("theme");
        var theme = result.Success ? result.Value : "light";
        if (theme != null) AppState.SetTheme(theme);
    }

    private async Task ToggleTheme()
    {
        switch (AppState.Theme)
        {
            case "dark":
                await Js.InvokeVoidAsync("toggleTheme", "dark");
                await ProtectedLocalStorage.SetAsync("theme", "light");
                AppState.SetTheme("light");
                break;
            default:
                await Js.InvokeVoidAsync("toggleTheme", "light");
                await ProtectedLocalStorage.SetAsync("theme", "dark");
                AppState.SetTheme("dark");
                break;
        }
    }
}
ThemeToggler.razor

We have injected the IJSRuntime so that we can call a javascript function, which will toggle the dark class on the document. For the javascript function i.e. toggleTheme, we have created an app.js file in the wwwroot and added the following script,

window.toggleTheme = function (theme) {
    switch (theme) {
        case "dark":
            document.documentElement.classList.remove("dark");
            break;
        default:
            document.documentElement.classList.add("dark");
            break;
    }
};
app.js

Add the script in the App.razor file. Notice that we have added some classes to the <body>.

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <base href="/" />
    <link rel="stylesheet" href="app.min.css" />
    <link rel="stylesheet" href="BlazorClassBasedThemeChanger.styles.css" />
    <script src="app.js"></script>
    <HeadOutlet @rendermode="InteractiveServer" />
</head>

<body class="tracking-tight antialiased text-gray-900 dark:text-slate-300 bg-white dark:bg-gray-800">
    <Routes @rendermode="InteractiveServer"/>
    <script src="_framework/blazor.web.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/flowbite/2.3.0/flowbite.min.js"></script>
</body>

</html>
flowbite is a component library based on Tailwind CSS. It is optional.

The AppState service is a class having a string property that stores the current theme. It is registered with the scoped lifecycle,

namespace BlazorClassBasedThemeChanger;

public class AppState
{
    public string Theme { get; private set; } = "light";
    
    public void SetTheme(string theme)
    {
        Theme = theme;
    }
}
AppState.cs
using BlazorClassBasedThemeChanger;
using BlazorClassBasedThemeChanger.Components;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();
builder.Services.AddScoped<AppState>();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
}

app.UseStaticFiles();
app.UseAntiforgery();

app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();

app.Run();
Program.cs

We should store the theme in the browser's local storage, so that when a user re-opens the app, the previously selected theme is persisted. Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage package could be used for that purpose.

Import it in the _Imports.razor file,

@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using BlazorClassBasedThemeChanger
@using BlazorClassBasedThemeChanger.Components
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
_Imports.razor

The tailwind.config.js file should look like this,

/** @type {import('tailwindcss').Config} */
const defaultTheme = require("tailwindcss/defaultTheme");
const colors = require("tailwindcss/colors");

module.exports = {
    content: ["./**/*.{razor,html,cshtml}", "./node_modules/flowbite/**/*.js"],
    theme: {
        extend: {
            colors: {
                primary: colors.purple,
                secondary: colors.blue,
            },
            fontFamily: {
                sans: ["'Inter Variable'", ...defaultTheme.fontFamily.sans],
            },
        },
    },
    plugins: [
        require("@tailwindcss/typography"),
        require('flowbite/plugin')
    ],
    darkMode: "class"
}
tailwind.config.js

That pretty much it! Download the source code,

https://github.com/fiyazbinhasan/BlazorClassBasedThemeChanger

Run the app and you should have the following UI. Use the toggle button to change between dark and light theme,

Light Theme
Dark Theme