Theme Building

Design custom themes with the visual builder and apply them across all BPT components.

Home / Docs / Theme Building

How Theming Works

Blazor Power Tools uses CSS custom properties (CSS variables) for theming. Every BPT component reads its colors, spacing, border radii, and other visual properties from a set of CSS variables. By changing these variables, you change the look of every component at once — no need to override individual component styles.

There are three ways to work with themes:

  1. Visual Theme Builder — A drag-and-drop GUI that generates theme files
  2. Manual CSS — Write CSS custom property overrides directly
  3. BptThemeLoader — Load and switch themes at runtime from CSS, JSON, or SCSS-ZIP bundles

Using the Visual Theme Builder

The BptThemeBuilder component is a built-in visual editor. It lets you pick colors, adjust spacing, set border radii, and preview changes in real time. When you're happy with the result, export the theme as a CSS file or JSON object.

1

Add the Theme Builder to a Page

<BptThemeBuilder OnThemeExported="HandleExport" /> @code { private void HandleExport(string themeCss) { // Save to file, database, or apply directly Console.WriteLine(themeCss); } }
2

Adjust Colors and Properties

The builder displays live previews of components as you adjust:

  • Primary, secondary, and accent colors
  • Background and surface colors
  • Text colors (primary, secondary, muted)
  • Border radius (sharp, rounded, pill)
  • Font family and sizing
  • Shadow intensity
3

Export and Apply

Click "Export" to generate either:

  • CSS file — Drop into your wwwroot and link in App.razor
  • JSON file — Load at runtime with BptThemeLoader
Try it now Open the live Theme Builder demo to see it in action.

Manual CSS Theming

Override BPT's default CSS custom properties in your own stylesheet:

/* my-theme.css */ :root { /* Primary brand colors */ --bpt-color-primary: #1565C0; --bpt-color-primary-light: #42A5F5; --bpt-color-primary-dark: #0D47A1; /* Secondary / accent */ --bpt-color-secondary: #FF6F00; --bpt-color-accent: #00BFA5; /* Backgrounds */ --bpt-bg-surface: #FFFFFF; --bpt-bg-elevated: #F5F5F5; --bpt-bg-overlay: rgba(0, 0, 0, 0.5); /* Text */ --bpt-text-primary: #212121; --bpt-text-secondary: #757575; --bpt-text-on-primary: #FFFFFF; /* Shape */ --bpt-border-radius: 8px; --bpt-border-radius-sm: 4px; --bpt-border-radius-lg: 16px; /* Shadows */ --bpt-shadow-sm: 0 1px 3px rgba(0,0,0,0.12); --bpt-shadow-md: 0 4px 12px rgba(0,0,0,0.15); --bpt-shadow-lg: 0 8px 24px rgba(0,0,0,0.2); /* Typography */ --bpt-font-family: 'Inter', 'Segoe UI', sans-serif; --bpt-font-size-base: 0.9375rem; }

Include the stylesheet in your App.razor after the BPT default styles:

<link href="_content/BlazorPowerTools/css/bpt.css" rel="stylesheet" /> <link href="css/my-theme.css" rel="stylesheet" />

A static <link> is the simplest option, but it locks the theme at page load. For anything more dynamic — switching themes at runtime, applying a JSON or SCSS-ZIP bundle exported from BptThemeBuilder, or loading per-tenant themes — use the BptThemeLoader component.

Loading themes at runtime

BptThemeLoader applies a theme by injecting CSS variables onto :root at runtime. It accepts three formats — CSS, JSON, or SCSS-ZIP — from three sources: URL, byte array, or base64 string. JSON is the recommended format because it preserves component-specific overrides that plain CSS cannot round-trip.

Headless mode — one theme, no UI

Drop the component anywhere in your tree and point it at a theme source:

<!-- From a URL (CSS, JSON, or .zip SCSS bundle) --> <BptThemeLoader Url="/themes/ocean.json" /> <!-- From raw bytes (e.g. a user-uploaded file) --> <BptThemeLoader Data="@_themeBytes" /> <!-- From a base64 string (e.g. stored in a database) --> <BptThemeLoader Base64Data="@_themeBase64" />

Built-in dropdown picker

Pass a dictionary of themes and BptThemeLoader renders a dropdown that switches between them — no extra wiring needed.

<BptThemeLoader Themes="@_themes" @bind-SelectedTheme="_currentTheme" Placeholder="Choose a theme" /> @code { private string _currentTheme = "/themes/default.json"; private Dictionary<string, string> _themes = new() { ["/themes/default.json"] = "Default", ["/themes/dark.json"] = "Dark", ["/themes/ocean.json"] = "Ocean", }; }

App-wide themes from the root component

Because the injected CSS variables live on :root, a single BptThemeLoader placed near the top of your app applies to every page. Put it in MainLayout.razor (or any layout/root component) and bind its source parameter to global state — for example a value held on BptRootComponentBase or any cascading state object.

<!-- MainLayout.razor --> <BptThemeLoader Url="@_currentThemeUrl" OnThemeChanged="OnThemeChanged" /> <div class="page"> @Body </div> @code { [CascadingParameter] public BptRootComponentBase? AppState { get; set; } private string _currentThemeUrl = "/themes/default.json"; private void OnThemeChanged(string url) { // Persist the user's selection on the root state so it survives navigation. _currentThemeUrl = url; } }

Programmatic loading via @ref

Capture a reference and call LoadFromUrlAsync, LoadFromBytesAsync, LoadFromBase64Async, or ApplyConfigAsync from C#. Use this when the theme source is decided by application logic — tenant config, A/B test bucket, an uploaded file, etc.

<BptThemeLoader @ref="_loader" AutoLoad="false" /> @code { private BptThemeLoader? _loader; private async Task LoadTenantTheme(string tenantId) { if (_loader is null) return; await _loader.LoadFromUrlAsync($"/api/tenants/{tenantId}/theme.json"); } }

Theme JSON files should be placed in wwwroot/themes/:

wwwroot/ themes/ default.json dark.json ocean.json forest.json

Each JSON file contains key-value pairs matching the CSS custom property names:

{ "bpt-color-primary": "#1565C0", "bpt-color-secondary": "#FF6F00", "bpt-bg-surface": "#FFFFFF", "bpt-text-primary": "#212121", "bpt-border-radius": "8px" }

Dark Mode

Implementing dark mode is straightforward — create a second set of CSS variables scoped to a class or media query:

/* Dark mode via class toggle */ .dark-theme { --bpt-bg-surface: #1E1E1E; --bpt-bg-elevated: #2D2D2D; --bpt-text-primary: #E0E0E0; --bpt-text-secondary: #A0A0A0; --bpt-color-primary: #64B5F6; --bpt-shadow-md: 0 4px 12px rgba(0,0,0,0.4); } /* Or auto-detect via system preference */ @media (prefers-color-scheme: dark) { :root { --bpt-bg-surface: #1E1E1E; /* ... */ } }

Advanced tutorial

Build a custom theme — end to end

A step-by-step walkthrough: from picking brand colors in the visual builder to shipping a JSON theme that switches at runtime via BptThemeLoader.

An unhandled error has occurred. Reload 🗙

Rejoining the server...

Rejoin failed... trying again in seconds.

Failed to rejoin.
Please retry or reload the page.

The session has been paused by the server.

Failed to resume the session.
Please reload the page.