ThemeSwitcher Component

An all-in-one popover panel that lets users pick a base color scheme and a primary accent color at runtime, with changes persisted across sessions via localStorage.

Demo

Click the circle button to open the theme panel. Changes apply immediately across the whole demo site.

Current theme

Base: Zinc  /  Primary: Default

DarkModeToggle

A companion component bundled in the same namespace. It uses the same ThemeService and is already embedded inside ThemeSwitcher's popover header, but can also be placed standalone anywhere in your layout.

Dark mode: off

@using BlazorUI.Components.Theme

<DarkModeToggle />

Usage

Minimal usage — drop it anywhere in your layout.

@using BlazorUI.Components.Theme

<ThemeSwitcher />

Typical placement is in a top-bar or sidebar footer alongside DarkModeToggle:

<header class="flex items-center gap-2">
    <ThemeSwitcher />
    <DarkModeToggle />
</header>

Customisation

Use TriggerClass and PopoverContentClass to tailor the trigger button and panel without subclassing.

<!-- Extra ring on trigger -->
<ThemeSwitcher TriggerClass="rounded-full ring-2 ring-primary ring-offset-2 ring-offset-background" />

<!-- Narrower popover panel -->
<ThemeSwitcher PopoverContentClass="!w-72" />

Fixed positioning

By default Strategy is Fixed so the popover and tooltip correctly escape any CSS stacking context (e.g. sidebars with overflow: hidden or transform). Switch to Absolute if you embed the switcher in a regular document flow where fixed positioning causes layout shifts.

@using BlazorUI.Primitives.Services   

<ThemeSwitcher Strategy="PositioningStrategy.Absolute" />

API

Parameter Type Default Description
TriggerClass string? Additional CSS classes merged onto the trigger Button
PopoverContentClass string? Additional CSS classes merged onto the PopoverContent panel
Strategy PositioningStrategy Fixed Forwarded to both the popover and the tooltip. Use Fixed inside transformed/overflow-hidden containers
ZIndex int ZIndexLevels.PopoverContent Z-index for the popover panel

ThemeService

Inject ThemeService directly if you need to read or change the theme programmatically without rendering the switcher UI.

Member Kind Description
CurrentBaseColor Property Currently active BaseColor
CurrentPrimaryColor Property Currently active PrimaryColor
IsDarkMode Property Whether dark mode is currently active
OnThemeChanged Event Fired whenever base color, primary color, or dark mode changes
InitializeAsync() Method Reads persisted preferences from localStorage and applies them
SetBaseColorAsync(BaseColor) Method Changes and persists the base color scheme
SetPrimaryColorAsync(PrimaryColor) Method Changes and persists the primary accent color
SetThemeAsync(bool isDark) Method Toggles dark mode and persists the preference
@inject ThemeService ThemeService

<!-- Read current values -->
<p>@ThemeService.CurrentBaseColor / @ThemeService.CurrentPrimaryColor</p>

@code {
    protected override async Task OnInitializedAsync()
    {
        await ThemeService.InitializeAsync();
        ThemeService.OnThemeChanged += () => InvokeAsync(StateHasChanged);
    }

    private Task ApplyGreenTheme() =>
        ThemeService.SetPrimaryColorAsync(PrimaryColor.Green);
}

App.razor Setup

The theme system requires a small amount of boilerplate in your host page. Add the following to the <head> of App.razor (Blazor Web) or index.html (Blazor WASM).

1 — Core stylesheet

The main BlazorUI stylesheet provides all component styles and CSS variables.

<link href="_content/NeoBlazorUI.Components/blazorui.css" rel="stylesheet" />

2 — Base color stylesheets

Each base color is a separate CSS file. In production include only the ones you actually offer to users — if your app only uses Zinc, ship only zinc.css.

<!-- Base color themes (include only those you need) -->
<link rel="stylesheet" href="_content/NeoBlazorUI.Components/css/themes/base/zinc.css" />
<link rel="stylesheet" href="_content/NeoBlazorUI.Components/css/themes/base/slate.css" />
<link rel="stylesheet" href="_content/NeoBlazorUI.Components/css/themes/base/gray.css" />
<link rel="stylesheet" href="_content/NeoBlazorUI.Components/css/themes/base/neutral.css" />
<link rel="stylesheet" href="_content/NeoBlazorUI.Components/css/themes/base/stone.css" />

3 — Primary color stylesheets

Same rule applies — only include the accent colors your app exposes. Omitting unused files reduces the CSS bundle size.

<!-- Primary color themes (include only those you need) -->
<link rel="stylesheet" href="_content/NeoBlazorUI.Components/css/themes/primary/red.css" />
<link rel="stylesheet" href="_content/NeoBlazorUI.Components/css/themes/primary/rose.css" />
<link rel="stylesheet" href="_content/NeoBlazorUI.Components/css/themes/primary/orange.css" />
<link rel="stylesheet" href="_content/NeoBlazorUI.Components/css/themes/primary/amber.css" />
<link rel="stylesheet" href="_content/NeoBlazorUI.Components/css/themes/primary/yellow.css" />
<link rel="stylesheet" href="_content/NeoBlazorUI.Components/css/themes/primary/lime.css" />
<link rel="stylesheet" href="_content/NeoBlazorUI.Components/css/themes/primary/green.css" />
<link rel="stylesheet" href="_content/NeoBlazorUI.Components/css/themes/primary/emerald.css" />
<link rel="stylesheet" href="_content/NeoBlazorUI.Components/css/themes/primary/teal.css" />
<link rel="stylesheet" href="_content/NeoBlazorUI.Components/css/themes/primary/cyan.css" />
<link rel="stylesheet" href="_content/NeoBlazorUI.Components/css/themes/primary/sky.css" />
<link rel="stylesheet" href="_content/NeoBlazorUI.Components/css/themes/primary/blue.css" />
<link rel="stylesheet" href="_content/NeoBlazorUI.Components/css/themes/primary/indigo.css" />
<link rel="stylesheet" href="_content/NeoBlazorUI.Components/css/themes/primary/violet.css" />
<link rel="stylesheet" href="_content/NeoBlazorUI.Components/css/themes/primary/purple.css" />
<link rel="stylesheet" href="_content/NeoBlazorUI.Components/css/themes/primary/fuchsia.css" />
<link rel="stylesheet" href="_content/NeoBlazorUI.Components/css/themes/primary/pink.css" />

4 — Theme JavaScript

The theme script must load before Blazor initialises to avoid a flash of unstyled content (FOUC). Place both tags at the end of <head>, after the stylesheets.

<!-- Theme JS — must come before Blazor boots to prevent FOUC -->
<script src="_content/NeoBlazorUI.Components/js/theme.js"></script>
<script>
    window.theme.initialize();
</script>

5 — Service registration

ThemeService is registered automatically when you call AddBlazorUIComponents() in Program.cs.

// Program.cs
builder.Services.AddBlazorUIPrimitives();
builder.Services.AddBlazorUIComponents(); // registers ThemeService
An unhandled error has occurred. Reload 🗙