BlazorNova is built around a small set of design principles that make components predictable, composable, and theme-adaptive — without requiring you to wire up colours or hover states manually.
Every piece of UI lives on a surface — a named palette that defines
background, text, input, and semantic colours for that visual context.
Wrap any region in a <BnSurface> and all descendant components
automatically inherit the appropriate palette depth.
Neutral surfaces form a nesting hierarchy: Surface0 → Surface1 → Surface2 → Surface3 → Surface1…
Each step represents one level of visual elevation — a page, then a panel, then a card,
then a row. You never set the depth manually; it falls out of the structure of your markup.
<!-- Page lives on Surface0 implicitly -->
<BnSurface> @* Surface1 — panel *@
<BnSurface> @* Surface2 — card *@
<BnSurface> @* Surface3 — row *@
<BnTextBox ... />
</BnSurface>
</BnSurface>
</BnSurface>
<!-- Force a branded context anywhere in the tree -->
<BnSurface SurfacePalette="@_engine.ThemePalette.SurfacePrimary1">
<BnButton Label="Primary Action" />
</BnSurface>
Branded surfaces (SurfacePrimary1/2, SurfaceSecondary1/2,
SurfaceTertiary1/2) represent visual intent rather than depth.
A region wrapped in SurfacePrimary1 signals a primary-branded context —
a hero section, a highlighted callout, or a primary action button.
The 1/2 pairs for branded surfaces exist to provide normal and hover/active
states (Primary1 = normal, Primary2 = hover), not to nest one
branded surface inside another.
The rel1 suffix means "one level deeper than my current surface."
For a component sitting on Surface1, bg_rel1_Bg resolves to
Surface2-Bg. For a component on SurfacePrimary1,
bg_rel1_Bg resolves to SurfacePrimary2-Bg.
This means hover states are written the same way everywhere — regardless of which surface a component happens to be placed on:
// Hover and selection — use EmphasisBg (within the same surface)
Bn.bg_Bg.text_OnBg
.Hover(h => h.bg_EmphasisBg)
// Alternating rows — use AltBg
Bn.bg_Bg.text_OnBg.Odd(o => o.bg_AltBg)
// Borders — use the Border token (softer than OnBg)
Bn.border.border_solid.border_Border
// Reserve rel1 for true depth nesting (child above parent)
A button, a list item, a card — all use the same hover expression. The resolved colour
adapts automatically to the containing surface at render time.
This is the core reason branded surfaces have a 1/2 pair: so there is always
a well-defined rel1 for hover, matching the same contract as neutral surfaces.
Components do not know — and should not care — which surface they are placed on.
They receive the current surface via a cascading parameter and use NextBn
to build their styles relative to it. NextBn always starts one level
deeper than the parent surface, so a component's background automatically sits above
its container without any explicit configuration.
@* Inside a component — never references a specific surface name *@
<div class="@NextBn.bg_Bg.text_OnBg.rounded_md.p_3
.Hover(h => h.bg_EmphasisBg)">
@ChildContent
</div>
The same component placed inside Surface1, SurfacePrimary1,
or SurfaceSecondary1 will always look correct — background, text, and
hover colours all resolve from the local palette context.
Each surface palette defines a focused set of semantic colour tokens:
Bg / OnBg — container background and its content colourAltBg — subtle alternate background (e.g. alternating table rows)EmphasisBg — emphasis for hover and selection statesBorder — soft mid-tone border colourInput / OnInput — input field background and textError, ErrorContainer, OnErrorContainerWarning, WarningContainer, OnWarningContainerSuccess, SuccessContainer, OnSuccessContainer
There is no per-surface Accent token. A "primary action" colour always
comes from SurfacePrimary1-Bg; secondary from SurfaceSecondary1-Bg.
This makes intent explicit: a button chooses its visual weight by declaring its variant
(ButtonVariant.Primary), not by inheriting whatever accent the parent
surface happens to define.
Semantic colours (Error, Warning, Success) remain per-surface because they need to be readable on top of each specific surface background. Brand colours do not have this dependency — they are always the same, regardless of context.
Every component exposes one or more BnModifier parameters for the
parts of its layout that are worth customising. A modifier wraps a styling function
and is applied on top of the component's defaults.
@* Caller overrides the card container *@
<BnCard ContainerModifier="@BnModifier.Create(x => x.rounded_xl.shadow_lg)" />
@* Inside the component, defaults merge with the caller's modifier *@
public BlazorNova ContainerBn =>
BnModifier.Create(x => x.rounded_md.p_4)
.OverrideWith(ContainerModifier);
Modifiers are additive — they run after the defaults, so you can extend, narrow, or
replace specific properties without rewriting the entire style chain. Components compose
their defaults with BnModifier.Create(...).OverrideWith(ContainerModifier)
so the caller always wins.
BlazorNova.New starts a fresh CSS class builder. Chain utility properties
and methods to compose the class string for any element.
Conditional classes, responsive breakpoints, and pseudo-state variants (Hover, Focus,
Disabled, etc.) are all first-class:
// Static layout — no surface needed
BlazorNova.New.flex.gap_4.items_center.p_3
// Conditional and responsive
BlazorNova.New
.w_full
.Md(x => x.w_[480px])
.If(() => isActive, x => x.font_bold)
.Hover(x => x.opacity_80)
// Surface-aware (requires SetSurface or NextBn)
NextBn.bg_Bg.text_OnBg.border_Border
.Hover(h => h.bg_EmphasisBg)
Surface-aware styles are produced when the builder has a surface set via
SetSurface() or via NextBn (which sets the surface automatically).
Plain BlazorNova.New is used for static, surface-independent styles such
as layout utilities.