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 ColorAdjustment — Tint 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.