# NeoUI -- Theming
NeoUI is 100% compatible with shadcn/ui themes. NeoUI v4.0.0 ships Theme v2: 10 base colors x 17 primary accents = 170 combinations, plus StyleVariant, RadiusPreset, FontPreset, MenuColor, MenuAccent, and ThemePreset bundling all dimensions atomically.
## How Theming Works
All NeoUI components use CSS custom properties (variables) in OKLCH color format for colors, spacing, and radius. Switching a theme means swapping variable definitions -- no recompiling, no class changes.
## AppProvider (Required in v4.0.0)
`AppProvider` must wrap everything in `MainLayout.razor`, including all portal hosts. It (1) initializes ThemeService and restores the persisted theme from localStorage, and (2) broadcasts the active StyleVariant as a CascadingValue. Components outside AppProvider receive StyleVariant.Default permanently.
```razor
@* MainLayout.razor *@
@Body
```
## CSS Load Order (v4.0.0)
Place in `App.razor` in this exact order:
```html
```
## Key CSS Variables
```css
:root {
/* Surfaces */
--background: /* page background */;
--foreground: /* body text */;
--card: /* card background */;
--card-foreground: /* card text */;
--popover: /* popover/dropdown background */;
--popover-foreground: /* popover text */;
/* Brand */
--primary: /* primary brand color */;
--primary-foreground: /* text on primary */;
--secondary: /* secondary surface */;
--secondary-foreground: /* text on secondary */;
--muted: /* muted background */;
--muted-foreground: /* muted text */;
--accent: /* hover/accent surface */;
--accent-foreground: /* text on accent */;
--destructive: /* error/danger color */;
--destructive-foreground: /* text on destructive */;
/* Structure */
--border: /* border color */;
--ring: /* focus ring color */;
--radius: /* base border radius */;
/* Sidebar */
--sidebar: /* sidebar background */;
--sidebar-foreground: /* sidebar text */;
--sidebar-primary: /* sidebar active item */;
--sidebar-accent: /* sidebar hover */;
--sidebar-border: /* sidebar border */;
/* Charts */
--chart-1 ... --chart-5: /* chart color palette */;
/* Extended tokens (v4.0.0) */
--surface: /* slightly elevated layer between background and card */;
--surface-foreground: /* text on surface */;
--code: /* code block background */;
--code-foreground: /* code block text */;
--code-highlight: /* highlighted line background in code blocks */;
--code-number: /* line numbers and dimmed code text */;
--selection: /* text selection highlight color */;
--selection-foreground: /* text color on selection highlight */;
/* Alerts */
--alert-info, --alert-info-foreground, --alert-info-bg;
--alert-warning, --alert-warning-foreground, --alert-warning-bg;
--alert-success, --alert-success-foreground, --alert-success-bg;
--alert-danger, --alert-danger-foreground, --alert-danger-bg;
/* Typography */
--font-sans: /* body font stack */;
--font-heading: /* heading font stack; falls back to --font-sans */;
/* Radius scale (proportional, safe at --radius: 0rem) */
--radius-xs: /* x0.4 */;
--radius-sm: /* x0.6 */;
--radius-md: /* x0.8 */;
--radius-lg: /* x1.0 (base) */;
--radius-xl: /* x1.4 */;
--radius-2xl: /* x1.8 */;
--radius-3xl: /* x2.2 */;
--radius-4xl: /* x2.6 */;
/* Shadows (set by StyleVariant; Luma uses soft diffuse OKLCH shadows) */
--shadow-xs --shadow-sm --shadow-md --shadow-lg --shadow-xl --shadow-2xl;
}
```
## Dark Mode
Dark mode variables are defined in each theme file under `.dark` / `[data-theme="dark"]`.
Use the built-in toggle:
```razor
```
Or `ThemeSwitcher` for full palette + style switching:
```razor
```
Register the theme service:
```csharp
builder.Services.AddNeoUIComponents(); // includes ThemeService
```
## StyleVariant Enum
Controls `--radius`, `--spacing-scale`, and shadow values. Broadcast as CascadingValue from AppProvider.
| Variant | --radius | --spacing-scale | Character |
|---------|------------|-----------------|----------------------------------|
| Default | unchanged | 1 | Backward-compatible (no file) |
| Vega | 0.625rem | 1 | Professional, balanced |
| Nova | 0.375rem | 0.85 | Compact, dashboard/admin |
| Maia | 1rem | 1.15 | Spacious, consumer-friendly |
| Lyra | 0rem | 1 | Sharp/boxy, developer tooling |
| Mira | 0.25rem | 0.7 | Ultra-dense, data-heavy |
| Luma | 0.75rem | 1 | Glassmorphism, modern SaaS |
CSS: `_content/NeoUI.Blazor/css/themes/styles/{vega,nova,maia,lyra,mira,luma}.css`
Luma extras: soft diffuse shadows, semi-transparent inputs via `color-mix`, stronger overlay blur (`--blur-sm: 4px` on `[data-slot="overlay"]`).
```csharp
await ThemeService.SetStyleVariantAsync(StyleVariant.Nova);
```
## RadiusPreset Enum
Independent named radius overrides; takes precedence over StyleVariant in the CSS cascade.
| Preset | Value |
|--------|----------------------------|
| None | 0rem |
| Small | 0.45rem |
| Medium | 0.625rem (default, no file)|
| Large | 0.875rem |
CSS: `_content/NeoUI.Blazor/css/themes/radius/{none,small,large}.css`
```csharp
await ThemeService.SetRadiusPresetAsync(RadiusPreset.Large);
```
## 7-Step Proportional Radius Scale
All scale steps derive from `--radius`; safe at `--radius: 0rem` (Lyra style).
| Variable | Multiplier |
|-------------|------------|
| --radius-xs | x0.4 |
| --radius-sm | x0.6 |
| --radius-md | x0.8 |
| --radius-lg | x1.0 (base)|
| --radius-xl | x1.4 |
| --radius-2xl| x1.8 |
| --radius-4xl| x2.6 |
## FontPreset Enum
6 curated font pairings setting `--font-sans` and `--font-heading`.
| Preset | Description |
|-------------|-------------------------------|
| System | system-ui stack (default, no file) |
| Inter | Inter |
| Geist | Geist |
| CalSans | CalSans |
| DmSans | DM Sans |
| PlusJakarta | Plus Jakarta Sans |
CSS: `_content/NeoUI.Blazor/css/themes/fonts/{inter,geist,calsans,dmsans,plusjakarta}.css`
**Note:** Font CSS files set CSS variables only. Load the actual font face files separately (e.g. Google Fonts `` tag or local @font-face declarations).
```csharp
await ThemeService.SetFontPresetAsync(FontPreset.Inter);
```
## --font-heading Variable
Falls back to `--font-sans` when not explicitly set. Font presets assign it independently to enable distinct heading typography.
## MenuColor Enum
Controls background treatment of all floating surfaces: Popover, DropdownMenu, Select, Combobox.
| Value | Treatment |
|----------------------|-------------------------------------------------------------|
| Default | Solid surface |
| Inverted | Dark surface (light mode only) |
| DefaultTranslucent | 50% opacity + blur(18px) + saturate(150%) |
| InvertedTranslucent | Dark + 70% opacity + blur (light mode only) |
```csharp
await ThemeService.SetMenuColorAsync(MenuColor.DefaultTranslucent);
```
## MenuAccent Enum
Controls hover intensity on menu items.
| Value | Color |
|--------|------------------------------------------|
| Subtle | --accent (default) |
| Bold | --primary (high-contrast brand hover) |
```csharp
await ThemeService.SetMenuAccentAsync(MenuAccent.Bold);
```
## BaseColor Enum (10 Total)
v4.0.0 adds 5 new chromatic neutrals.
| Color | Character |
|---------|-----------------------------------|
| Zinc | Neutral gray (default) |
| Slate | Cool blue-gray |
| Gray | Neutral gray |
| Neutral | Warm gray |
| Stone | Warm brownish |
| Luma | Blue-indigo tinted (new in v4) |
| Mist | Cool blue-gray (new in v4) |
| Mauve | Warm purple-gray (new in v4) |
| Taupe | Warm brownish-gray (new in v4) |
| Olive | Muted green-gray (new in v4) |
CSS: `_content/NeoUI.Blazor/css/themes/base/{luma,mist,mauve,taupe,olive}.css`
Total combinations: 10 base x 17 primary = **170**
```csharp
await ThemeService.SetBaseColorAsync(BaseColor.Luma);
```
## ThemePreset Record
Bundles all 8 theme dimensions atomically: BaseColor, PrimaryColor, StyleVariant, RadiusPreset, FontPreset, MenuAccent, MenuColor, and Name.
### Built-in Presets
| Preset | Base | Style | Radius | Font |
|---------|-------|---------|--------|-------------|
| Default | Zinc | Default | Medium | System |
| Luma | Zinc | Luma | Medium | Inter |
| Nova | Zinc | Nova | Small | Geist |
| Maia | Mauve | Maia | Large | PlusJakarta |
| Lyra | Slate | Lyra | None | Geist |
```csharp
// Apply a built-in preset
await ThemeService.ApplyPresetAsync(ThemePreset.Luma);
// Create and apply a custom preset
var custom = new ThemePreset(
Name: "Glass Dashboard",
BaseColor: BaseColor.Luma,
PrimaryColor: PrimaryColor.Blue,
StyleVariant: StyleVariant.Luma,
RadiusPreset: RadiusPreset.Medium,
FontPreset: FontPreset.Inter,
MenuAccent: MenuAccent.Bold,
MenuColor: MenuColor.DefaultTranslucent
);
await ThemeService.ApplyPresetAsync(custom);
```
## ThemeService API
```razor
@inject IThemeService ThemeService
```
### Properties
| Property | Type |
|-----------------------|----------------|
| CurrentBaseColor | BaseColor |
| CurrentPrimaryColor | PrimaryColor |
| CurrentStyleVariant | StyleVariant |
| CurrentRadiusPreset | RadiusPreset |
| CurrentFontPreset | FontPreset |
| CurrentMenuAccent | MenuAccent |
| CurrentMenuColor | MenuColor |
### Methods
| Method | Description |
|-------------------------------------|------------------------------------------|
| SetBaseColorAsync(BaseColor) | Switch base palette |
| SetPrimaryColorAsync(PrimaryColor) | Switch primary accent |
| SetStyleVariantAsync(StyleVariant) | Switch style variant |
| SetRadiusPresetAsync(RadiusPreset) | Switch radius preset |
| SetFontPresetAsync(FontPreset) | Switch font preset |
| SetMenuAccentAsync(MenuAccent) | Switch menu item hover intensity |
| SetMenuColorAsync(MenuColor) | Switch floating surface treatment |
| ApplyPresetAsync(ThemePreset) | Apply all 8 dimensions atomically |
| SetTheme(base, primary) | Legacy method -- still works |
### Runtime Switching Examples
```razor
@inject IThemeService ThemeService
```
## ThemeSwitcher Component
```razor
```
| Parameter | Type | Default | Description |
|--------------------|---------------------|---------|-------------------------------------------------------|
| ShowStyles | bool | false | Adds Styles tab (variant, radius, font, menu options) |
| Strategy | PositioningStrategy | Fixed | Popover positioning strategy |
| ZIndex | int | 60 | z-index of the switcher popover |
| TriggerClass | string? | null | CSS classes on the trigger button |
| PopoverContentClass| string? | null | CSS classes on the popover content |
| Align | PopoverAlign | End | Popover alignment |
Trigger button shows a two-tone swatch: current base color (fill) + current primary (border). With `ShowStyles="true"`: Colors tab (base + primary selectors) + Styles tab (variant, radius, font, menu accent, menu color).
## Custom Theme (OKLCH Format)
NeoUI v4 uses OKLCH color format internally:
```css
/* wwwroot/css/my-theme.css */
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--primary: oklch(0.623 0.214 259.8);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.96 0 0);
--muted: oklch(0.96 0 0);
--accent: oklch(0.96 0 0);
--destructive: oklch(0.577 0.245 27.3);
--border: oklch(0.92 0 0);
--ring: oklch(0.623 0.214 259.8);
--radius: 0.625rem;
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
/* ... */
}
```
## shadcn/ui Compatibility
NeoUI is 100% compatible with shadcn/ui themes. Use any theme from https://ui.shadcn.com/themes or https://tweakcn.com directly. shadcn/ui themes use HSL format; NeoUI's own files use OKLCH. Both formats work -- CSS custom properties accept either.
## Tailwind v4 Integration
NeoUI's pre-built CSS works without Tailwind. If using Tailwind v4 for custom styles, you **must** add `@theme inline` to your Tailwind build input. Without it, Tailwind v4 emits hardcoded `oklch()` values that override NeoUI's runtime CSS variable changes:
```css
/* app.css */
@import "tailwindcss";
@theme inline {
/* your customizations */
}
```