Theming

Customize the look and feel of Blazor Blueprint components using CSS variables.

Overview

Blazor Blueprint uses CSS custom properties (variables) with the OKLCH color space for theming, making it easy to customize colors, typography, and other design tokens. The theming system is compatible with shadcn/ui themes from tweakcn.com and ui.shadcn.com/themes.

Quick Start

To create a custom theme, create a CSS file with your theme variables and load it before the BlazorBlueprint stylesheet. Since v4 every utility in blazorblueprint.css is prefixed bb:, so load order no longer matters for utilities — but your variables must still come first:

index.html
<head>
  <!-- 1. Your theme variables. Load BEFORE blazorblueprint.css. -->
  <link href="css/my-theme.css" rel="stylesheet" />

  <!-- 2. Your own Tailwind build output, if you have one. Any position. -->
  <link href="css/app.css" rel="stylesheet" />

  <!-- 3. Required: BlazorBlueprint styles. -->
  <link href="_content/BlazorBlueprint.Components/blazorblueprint.css" rel="stylesheet" />

  <!-- 4. Optional: the base and primary colour presets ThemeService and
          BbThemeSwitcher drive through data-base-color / data-primary-color. -->
  <link href="_content/BlazorBlueprint.Components/css/themes.css" rel="stylesheet" />

  <!-- 5. Recommended: applies the saved theme before the first paint, so the
          page does not flash the wrong one. Blocking script, after the styles. -->
  <script src="_content/BlazorBlueprint.Components/js/theme-init.js"></script>
</head>

Define your theme variables for both light mode (:root) and dark mode (.dark):

my-theme.css
/* my-theme.css */
:root {
  --primary: oklch(0.6 0.2 250);
  --primary-foreground: oklch(0.98 0 0);
  /* ... other light mode variables */
}

.dark {
  --primary: oklch(0.7 0.18 250);
  --primary-foreground: oklch(0.1 0 0);
  /* ... other dark mode variables */
}

Color Format

BlazorBlueprint uses the OKLCH color space for perceptually uniform colors. The format is:

oklch(lightness chroma hue)
  • Lightness (0-1): How light or dark the color is (0 = black, 1 = white)
  • Chroma (0-0.4+): Color intensity/saturation (0 = gray, higher = more vivid)
  • Hue (0-360): The color angle on the color wheel (0 = red, 120 = green, 240 = blue)

OKLCH provides better perceptual uniformity than HSL, meaning colors with the same lightness value actually appear equally bright. Alternative CSS formats (hex, rgb, hsl) are also supported if you prefer.

CSS Variables

Here are the CSS variables used by Blazor Blueprint components:

theme.css
/* Light Mode (Default) - Using OKLCH color space */
:root {
  /* Core colors */
  --background: oklch(1 0 0);
  --foreground: oklch(0.145 0 0);
  --card: oklch(1 0 0);
  --card-foreground: oklch(0.145 0 0);
  --popover: oklch(1 0 0);
  --popover-foreground: oklch(0.145 0 0);
  --primary: oklch(0.205 0 0);
  --primary-foreground: oklch(0.985 0 0);
  --secondary: oklch(0.97 0 0);
  --secondary-foreground: oklch(0.205 0 0);
  --muted: oklch(0.97 0 0);
  --muted-foreground: oklch(0.556 0 0);
  --accent: oklch(0.97 0 0);
  --accent-foreground: oklch(0.205 0 0);
  --destructive: oklch(0.577 0.245 27.325);
  --destructive-foreground: oklch(1 0 0);
  --border: oklch(0.922 0 0);
  --input: oklch(0.922 0 0);
  --ring: oklch(0.708 0 0);

  /* Alert colors */
  --alert-success: oklch(0.55 0.20 142);
  --alert-success-foreground: oklch(0.30 0.09 142);
  --alert-success-bg: oklch(0.993 0.003 142);
  --alert-info: oklch(0.50 0.20 255);
  --alert-info-foreground: oklch(0.30 0.10 255);
  --alert-info-bg: oklch(0.993 0.003 255);
  --alert-warning: oklch(0.68 0.18 55);
  --alert-warning-foreground: oklch(0.35 0.10 55);
  --alert-warning-bg: oklch(0.995 0.003 55);
  --alert-danger: oklch(0.55 0.22 27);
  --alert-danger-foreground: oklch(0.30 0.12 27);
  --alert-danger-bg: oklch(0.993 0.003 27);

  /* Chart colors */
  --chart-1: oklch(0.81 0.10 252);
  --chart-2: oklch(0.62 0.19 260);
  --chart-3: oklch(0.55 0.22 263);
  --chart-4: oklch(0.49 0.22 264);
  --chart-5: oklch(0.42 0.18 266);

  /* Sidebar colors */
  --sidebar: oklch(0.985 0 0);
  --sidebar-foreground: oklch(0.145 0 0);
  --sidebar-primary: oklch(0.205 0 0);
  --sidebar-primary-foreground: oklch(0.985 0 0);
  --sidebar-accent: oklch(0.97 0 0);
  --sidebar-accent-foreground: oklch(0.205 0 0);
  --sidebar-border: oklch(0.922 0 0);
  --sidebar-ring: oklch(0.708 0 0);

  /* Typography & Layout */
  --radius: 0.625rem;
  --font-sans: ui-sans-serif, system-ui, sans-serif;
  --font-serif: ui-serif, Georgia, serif;
  --font-mono: ui-monospace, monospace;

  /* Sidebar Dimensions */
  --sidebar-width: 16rem;
  --sidebar-width-mobile: 18rem;
  --sidebar-width-icon: 3rem;
}

/* Dark Mode */
.dark {
  /* Core colors */
  --background: oklch(0.145 0 0);
  --foreground: oklch(0.985 0 0);
  --card: oklch(0.205 0 0);
  --card-foreground: oklch(0.985 0 0);
  --popover: oklch(0.269 0 0);
  --popover-foreground: oklch(0.985 0 0);
  --primary: oklch(0.922 0 0);
  --primary-foreground: oklch(0.205 0 0);
  --secondary: oklch(0.269 0 0);
  --secondary-foreground: oklch(0.985 0 0);
  --muted: oklch(0.269 0 0);
  --muted-foreground: oklch(0.708 0 0);
  --accent: oklch(0.371 0 0);
  --accent-foreground: oklch(0.985 0 0);
  --destructive: oklch(0.704 0.191 22.216);
  --destructive-foreground: oklch(0.985 0 0);
  --border: oklch(0.275 0 0);
  --input: oklch(0.325 0 0);
  --ring: oklch(0.556 0 0);

  /* Alert colors */
  --alert-success: oklch(0.60 0.18 142);
  --alert-success-foreground: oklch(0.90 0.06 142);
  --alert-success-bg: oklch(0.18 0.008 142);
  --alert-info: oklch(0.55 0.18 255);
  --alert-info-foreground: oklch(0.90 0.05 255);
  --alert-info-bg: oklch(0.18 0.008 255);
  --alert-warning: oklch(0.65 0.16 55);
  --alert-warning-foreground: oklch(0.92 0.06 55);
  --alert-warning-bg: oklch(0.19 0.008 55);
  --alert-danger: oklch(0.55 0.20 27);
  --alert-danger-foreground: oklch(0.90 0.06 27);
  --alert-danger-bg: oklch(0.18 0.008 27);

  /* Sidebar colors */
  --sidebar: oklch(0.205 0 0);
  --sidebar-foreground: oklch(0.985 0 0);
  --sidebar-primary: oklch(0.488 0.243 264.376);
  --sidebar-primary-foreground: oklch(0.985 0 0);
  --sidebar-accent: oklch(0.269 0 0);
  --sidebar-accent-foreground: oklch(0.985 0 0);
  --sidebar-border: oklch(0.275 0 0);
  --sidebar-ring: oklch(0.439 0 0);
}

Dark Mode

Dark mode is the .dark class on the root HTML element. You do not have to drive that yourself — the library ships two components and a service that do it for you, and persist the choice:

@* A standalone light/dark button *@
<BbDarkModeToggle />

@* Or the full theme popover, which includes the same toggle *@
<BbThemeSwitcher />

@* Or drive it yourself through the service *@
@inject ThemeService Theme

<BbButton OnClick="@(() => Theme.SetDarkModeAsync(!Theme.IsDarkMode))">
    Toggle dark mode
</BbButton>

BbDarkModeToggle is a standalone button showing a sun in light mode and a moon in dark mode. It only toggles the dark class, so it works without themes.css. BbThemeSwitcher is a popover with colours, radius and the same light/dark toggle. Both call ThemeService.InitializeAsync on first render, and both are in this site's header.

Add theme-init.js to <head> (see Quick Start) so the saved mode is applied before the first paint, rather than flashing the wrong one.

Writing Direction (RTL)

Direction is part of how a theme reads, so it belongs beside dark mode rather than in a separate corner. Every spacing, border, radius and alignment utility the library emits is logical (margin-inline-start rather than margin-left), so a dir attribute mirrors the whole layout. There is no second stylesheet and no runtime branching.

Wrap your layout in BbDirectionProvider, including BbPortalHost, so overlays mirror too:

@* MainLayout.razor *@
@using BlazorBlueprint.Primitives

<BbDirectionProvider Direction="TextDirection.RightToLeft">
    <div class="min-h-screen bg-background">
        @Body
    </div>

    @* The portal host needs the direction too, or overlays stay unmirrored. *@
    <BbPortalHost />
    <BbToastProvider />
</BbDirectionProvider>

TextDirection.Auto is the default and follows CultureInfo.CurrentCulture, which is what every component already did — so adding the provider changes nothing until your application asks for a direction.

CSS Layer Priority

Blazor Blueprint components respect the CSS cascade, allowing your theme overrides to take precedence. By loading your theme CSS before the Blazor Blueprint stylesheet, your custom property values will be used throughout all components.

You can also override specific component styles by targeting them directly in your CSS:

/* Override specific component styles */
.btn-primary {
  /* Your custom button styles */
}

/* Or use CSS custom properties for broader changes */
:root {
  --primary: oklch(0.55 0.25 260);
}

Migration from HSL

If you have an existing theme using HSL colors, you can convert it to OKLCH for better color consistency. Here's a general conversion approach:

/* HSL format (old) */
--primary: hsl(221.2, 83.2%, 53.3%);

/* OKLCH format (new) */
--primary: oklch(0.488 0.243 264.376);

/* Conversion tips:
   - HSL lightness 50% ≈ OKLCH lightness 0.5-0.6
   - HSL saturation maps roughly to OKLCH chroma (0-100% → 0-0.3)
   - HSL hue maps directly to OKLCH hue (both 0-360)
*/

For accurate conversions, use online tools like oklch.com or colorjs.io. You can also continue using HSL values directly if you prefer—Blazor Blueprint supports any valid CSS color format.

Preset Themes

Blazor Blueprint includes several preset themes you can use out of the box:

Midnight
Ocean
Sunset
Amethyst
Cherry
Sunshine
Forest
Storm

Theme Details

Click on a theme below to view its CSS variables:

Midnight
Ocean
Sunset
Amethyst
Cherry
Sunshine
Forest
Storm