NE.Standard

Theming

A theme is a fixed set of tokens configured once on the application builder; a component never invents its own way of being themed. There are three vocabularies for "what does this look like", and every component uses the one its kind is entitled to.

Configuring the theme

ConfigureTheme on the application builder, through UIApplicationThemeBuilder:

builder.ConfigureTheme(theme => theme
    .ConfigureLightPalette(palette => palette with { Primary = new ColorVariant(ColorName.AstralTeal) })
    .ConfigureDarkPalette(palette => palette with { Primary = new ColorVariant(ColorName.AstralTeal, ColorAdjustment.Tint, 2) })
    .ConfigureTypography(typography => typography with { FontFamily = "Inter" })
    .ConfigureShape(shape => shape with { CardRadius = UICornerRadius.Uniform(12d) })
);
Method Does
UseTheme(UITheme) replaces the whole theme
UseLightPalette/UseDarkPalette(UIColorPalette) replaces one palette
ConfigureLightPalette/ConfigureDarkPalette(Func<UIColorPalette, UIColorPalette>) edits one palette from its current value
UseTypography/ConfigureTypography replaces or edits UITypography
UseShape/ConfigureShape replaces or edits UIShape

Every token type is a record with init-only members, so ConfigureXxx is always x => x with { ... } over the value passed in. UseTheme/UseLightPalette/etc. validate what they are given and throw if it is invalid.

UITheme

Light and Dark (UIColorPalette, both required), Typography (UITypography), Shape (UIShape), FocusRing (bool, off by default — a keyboard-navigated application turns it on for the whole theme; off because it can land oddly over a control that already shows its own "current" state), and PressRipple (bool, on by default — a radial wash from the pointer's own position on buttons, actions and menu entries; the web writes data-ui-press-ripple on <html> only while it is on, and respects prefers-reduced-motion, so a theme that wants presses drawn another way turns it off). UIThemeDefaults.Default is the starting point ConfigureTheme edits from, with LightPalette/DarkPalette/Typography/Shape also reachable individually.

UIColorPalette

One palette per mode. The roles: Primary, Accent, Background, Surface, Info, Warning, Success, Danger, each with an On* counterpart meant to sit on top of it (OnPrimary, OnAccent, OnBackground, OnSurface, OnInfo, OnWarning, OnSuccess, OnDanger); plus Selected, FocusRing, Border, Shadow, Overlay, and DisabledOpacity (a byte, 0–255, applied to disabled interactive elements).

Six roles also carry an ink variant — PrimaryInk, AccentInk, InfoInk, WarningInk, SuccessInk, DangerInk. The fill (Primary, Info, ...) is what a background or a filled control paints; the ink is what that same colour looks like as text on the page — a label, an icon, a badge's text — read against the page's own ground rather than against the fill's On* counterpart. WarningInk differs most from Warning because gold text at 1.09:1 contrast against a light page needs shading before it reads at all.

UITypography and UIShape

UITypography: FontFamily (string, default "Inter" — the web platform ships that face, so the default reads the same on every machine; another family is the page's own to provide) and six UITextStyle roles — Display, Title, Subtitle, Body, Caption, Overline. Each UITextStyle is FontSize, LineHeight, FontWeight (default 400) and an optional LetterSpacing, all in points.

UIShape: five UICornerRadius tokens — CardRadius, ButtonRadius, InputRadius, RowRadius (square by default — a key/value list or an action row), NotificationRadius (square by default, like a row).

Naming a colour

UIThemeColor is what a component property takes wherever a colour is themeable — a semantic role tracked live, or a fixed override:

new TextComponent().SetDescriptionColor(UIThemeColor.Muted);
new BadgeComponent().SetColor(UIThemeColor.FromColorVariant(ColorName.AstralTeal, ColorAdjustment.Tint, 2));

UIThemeColor.FromStyle(UIColorStyle) tracks a semantic role (Primary, Danger, Muted, ...) live across a theme change; the static properties (UIThemeColor.Primary, UIThemeColor.Danger, UIThemeColor.Muted, ...) are shorthand for exactly that. FromColorVariant fixes a colour from NE.Colors — a ColorName plus an optional ColorAdjustment, factor and opacity — used identically in both modes; Create(light, dark) gives each mode its own fixed colour. UIColorStyle itself is the full set of semantic roles a UIThemeColor or a palette member can name.

Muted is the one role measured from something else: it is a fraction of the ground the element sits on, not of the element's own colour. That matters because it has to be safe applied twice — inside a region a component already paints muted, a second muting would otherwise compound past the contrast a reader needs. A surface with an ink of its own sets --ui-faint-base to that ink and gets a faint version of it; everything else measures from the surface.

The colour vocabulary

The names themselves come from NE.Colors, a package of its own with no dependencies: thirty colours in nine families, each adjustable by a tint or a shade in ten steps and by an opacity.

Family Names
Neutral IronFog SilverNight BronzeDusk
Red / Pink StellarRed NebulaRose LunarPink
Orange / Yellow SolarAmber NebulaGold LunarYellow
Olive / Lime EclipseOlive NebulaLime LunarSage
Green AuroraGreen NebulaMint LunarFern
Teal / Cyan AstralTeal NebulaCyan LunarMoss
Blue QuantumBlue NebulaAqua LunarAzure
Purple NovaPurple NebulaViolet LunarLavender
Accent Comet Flare Ember Photon Vortex Halo

A ColorVariant is a name plus an optional ColorAdjustmentTint towards white, Shade towards black, a factor from 0 to 10 — and an opacity. That is the whole model: new ColorVariant(ColorName.AstralTeal, ColorAdjustment.Tint, 2) is the same colour two steps lighter.

Every colour with its full ramp is rendered in that package's own examples/palette.html, generated from the palette itself. What each name means is documented there rather than here, since the palette is used outside this framework as well.

The three looks

Kind Property Values
Containers and content-bearing surfaces Surface (UISurfaceStyle) Background — the page's own ground inside a border; Raised — a panel lifted off it, with its own fill and shadow; Tinted — the surface's own colour mixed into the page
Inputs with a field of their own Appearance (UIInputAppearance) Filled (default) — one fill, no edge; Outline — no fill, a border all round; Underline — no fill, one rule under the text; Ghost — no box until touched, for a field standing in a row
Buttons and badges Type (UIButtonType) on a button, Style (UIBadgeType) on a badge Buttons: Primary, Accent, Danger, Outline, Ghost, Link, Surface. Badges: Primary, Accent, Info, Warning, Success, Danger, Surface
How much room a button takes Size (UIButtonSize) Small — inside a row, a toolbar, a list; Medium — a button standing on its own; Large — a call to action

Surface is for things that are a surface — a card, an expander, a dialog. A layout panel draws nothing by default and takes an arbitrary Background colour instead where a page genuinely wants one there. A colour a person picked through a colour input is not judged as a theme role: what reads on top of it is decided by UIColorContrast.IsLight against the colour itself, not by the ambient theme.

Selection

ISelectionStyleComponent.SelectionStyle (UISelectionStyle?) says what a chosen row or item looks like, on ItemsViewComponent, TableComponent, TreeComponent, ButtonGroupComponent, MenuComponent and TabsComponent. UISelectionStyle carries Background, Foreground, Mark (UISelectionMark), MarkColor and Bold — any part left unset keeps the control's own default. UISelectionStyle.Ground(background) marks a choice by its fill alone; UISelectionStyle.Marked(mark, markColor) marks it with a line on one edge instead.

Theme mode

UIThemeMode is Light or Dark — no System member; null means "inherit" on a component and "follow the platform" on a session.

A component subtree can be forced into one mode regardless of the session's: IVisualComponent.Theme (UIThemeMode?), on every visual component, null by default to inherit the ambient theme.

For the whole session, a controller changes it through Context.UpdateSessionAsync:

await Context.UpdateSessionAsync(session => session with { ThemeMode = UIThemeMode.Dark }, cancellationToken);

ThemeSwitcherComponent is the built-in control for a person to flip it themselves — a button that renders both glyphs (LightIcon, DarkIcon, sized by IconSize) and lets the stylesheet pick which one shows, since the page's shell markup is cached across themes; the switch itself runs entirely on the client. SetThemeEffect(UIThemeMode? mode) is the effect behind it, and the one client effect with a server side: besides putting the client into the theme, it reports back to the session (WebUIHub.SetThemeAsync) so the next render agrees — a theme is a session setting, not just a client update, and Mode = null follows the platform's preference again.