Chrome, Skins & Themes
How every Beep* control gets its look: the
BeepChrome attached-property mixin, the chrome-token system, the ~67 per-skin
style dictionaries, and the one manager that switches them app-wide. A theme is the theme
for a skin — read that distinction first.
TheTechIdea.Beep.WPF.Controls.BeepChrome ·
TheTechIdea.Beep.WPF.Controls.Styling.BeepControlStyle ·
TheTechIdea.Beep.WPF.Controls.ThemeManagement.WpfThemesManager
On this page
Theme vs Skin
These are two different things with one relation: a theme is the theme for a
skin. Get this right before touching anything under Themes/.
| Term | What it is | Lives in |
|---|---|---|
| Skin | The design system itself — one entry of the BeepControlStyle enum
(Material3, Fluent, Apple, Dracula, …). Holds one keyed style per
control. |
Themes/Styles/{Skin}.xaml |
| Theme | The colors and fonts of a skin, defined across all controls. Each
skin bakes its paired theme's palette (AppleWpfTheme's accent
#007AFF is Apple.xaml's AppleAccent). |
Themes/Themes/ |
Because the skin bakes the theme's palette, naming a theme selects the matching
skin, and setting the skin is what puts the theme's colors and fonts on every
control. This mirrors the WinForms pattern where the ControlStyle setter drives
the Theme.
Themes/, distinct from both:
ControlDefaults.xaml— per-control chrome tokens (BeepButton.Background,BeepButton.CornerRadius, …).Light.xaml/Colors.xaml— color primitives (BeepAccentBrush,BeepSurfaceBrush, …).Generic.xaml— the WPF default-style entry point (via[assembly: ThemeInfo]), whereDefaultStyleKeyPropertyresolves. It merges the token chain and pins Material3 as the frozen startup default skin.
BeepChrome attached properties
BeepChrome is a static class of attached DPs usable on any
DependencyObject — the chrome "mixin" that any WPF primitive can host. A control
opts in by exposing CLR wrappers (ControlStyle/Theme) that forward to
these attached properties.
| Attached DP | Type | Purpose |
|---|---|---|
ControlStyle | BeepControlStyle | The skin. Inherits; default None. Change fires OnControlStyleChanged, which loads the per-skin RD and applies that skin's keyed style. |
Theme | string | Inherits; default "Light". Change fires OnThemeChanged, which resolves the theme name to its paired skin and applies it. |
IsBorderAffectedByTheme | bool | Whether the border chrome follows the theme. Inherits; default true. |
IsShadowAffectedByTheme | bool | Whether the drop shadow follows the theme. Inherits; default true. |
IsRoundedAffectedByTheme | bool | Whether the corner radius follows the theme. Inherits; default true. |
HasError | bool | Validation flag the template's error chrome reacts to. |
HoverBackground | Brush | Per-instance hover-state override. |
PressedBackground | Brush | Per-instance pressed-state override. |
FocusBackground | Brush | Per-instance focus-state override. |
DisabledBackground | Brush | Per-instance disabled-state override. |
Background / Foreground / BorderBrush / BorderThickness / CornerRadius / Padding | Brush / Thickness / CornerRadius | Inheriting common-chrome overrides. Use sparingly — the template + skin are normally enough. |
ComponentName | string | Diagnostic identifier for a chrome element. |
ControlDefaults.xaml tokens. Set a BeepChrome.*Background only when a
single instance must deviate. There is no ErrorIndicatorVisibility attached DP on
BeepChrome; validation chrome is driven by HasError plus the field
presenter's own error state.
A control opts in with plain CLR wrappers over the attached DPs:
public BeepControlStyle ControlStyle
{
get => BeepChrome.GetControlStyle(this);
set => BeepChrome.SetControlStyle(this, value);
}
public string Theme
{
get => BeepChrome.GetTheme(this);
set => BeepChrome.SetTheme(this, value);
}
How ControlStyle drives the look
ControlStyle is an inherited attached property. Set it once at a
window root and every control in the tree re-skins on its own:
<Window xmlns:beep="clr-namespace:TheTechIdea.Beep.WPF.Controls;assembly=TheTechIdea.Beep.WPF.Controls"
beep:BeepChrome.ControlStyle="Dracula">
<!-- every Beep control below inherits ControlStyle=Dracula -->
</Window>
When the value changes on an element, BeepChrome.OnControlStyleChanged runs a
two-step sequence:
EnsureSkinLoaded(style)— mergespack://application:,,,/TheTechIdea.Beep.WPF.Controls;component/Themes/Styles/{skin}.xamlintoApplication.Resources, removing the previously active skin by reference (the skins share no marker key, so a key-probe would leak a dictionary on every switch). It no-ops when that skin is already active — the property inherits, so the callback fires once per element, but the skin must load only once per style.ApplySkinStyle(d, style)— walks up the element's type hierarchy and assigns the skin's keyed style for that control type, e.g. aBeepButtonunder Material3 getsMaterial3BeepButtonStyle. A derived control falls back to its base control's skin style.
None is a real, meaningful state. The default is
BeepControlStyle.None, not a real skin. A DP callback only fires on a
change, so if the default were (say) Material3, explicitly selecting Material3 would be
a no-op and skin nothing. With None, setting it clears any local style and the
control falls back to the implicit default skin declared in Generic.xaml.
ApplySkinStyle returns early when TemplatedParent is a templated
control (the template already owns those elements — e.g. the BeepImage icon slots
inside a BeepTextBox template). But when TemplatedParent is a
ContentPresenter (DataTemplate content — cards, list rows), it is
restyled; skipping it would freeze every ItemTemplate's Beep controls on the
startup default.
The chrome-token system
No control template hardcodes a color. Every visual surface resolves from a chrome
token via DynamicResource, and the tokens live in one place.
Themes/ControlDefaults.xaml is the single source of the per-control
chrome tokens. Keys are named Beep{Control}.{Property} and bridge to the shared
style tokens:
<!-- ControlDefaults.xaml — the per-control token layer -->
<SolidColorBrush x:Key="BeepButton.Background" Color="{DynamicResource BeepStyleAccentBrush}" />
<SolidColorBrush x:Key="BeepButton.Foreground" Color="{DynamicResource BeepStyleAccentForegroundBrush}" />
<SolidColorBrush x:Key="BeepButton.HoverBackground" Color="{DynamicResource BeepStyleAccentBrush}" />
<SolidColorBrush x:Key="BeepButton.PressedBackground" Color="{DynamicResource BeepStyleAccentBrush}" />
<CornerRadius x:Key="BeepButton.CornerRadius">{DynamicResource ControlCornerRadius}</CornerRadius>
<Thickness x:Key="BeepButton.Padding">{DynamicResource ControlPadding}</Thickness>
A control template references these tokens — never a literal color:
<Border Background="{DynamicResource BeepButton.Background}"
CornerRadius="{DynamicResource BeepButton.CornerRadius}"
Padding="{DynamicResource BeepButton.Padding}">
<ContentPresenter .../>
</Border>
The token values in ControlDefaults.xaml are fallbacks. They
bridge to the shared style tokens (BeepStyle*Brush in
Themes/Styles/_Tokens.xaml), which in turn bridge to the color primitives in
Light.xaml. The merge order in Generic.xaml is exactly this token
dependency chain, each layer resolving against the one above:
| Layer | File | Example key |
|---|---|---|
| 1 · color primitives | Themes/Light.xaml | BeepAccentBrush |
| 2 · style tokens | Themes/Styles/_Tokens.xaml | BeepStyleAccentBrush |
| 3 · per-control chrome tokens | Themes/ControlDefaults.xaml | BeepButton.Background |
| 4 · the active skin | Themes/Styles/{Skin}.xaml | Material3BeepButtonStyle + palette |
DynamicResource Beep{Name}.{Token} lookup finds nothing, it returns null and that
surface stays unpainted. Every token a template references must exist in
ControlDefaults.xaml (or be overridden with the same key name in a
per-skin RD). Don't put tokens in individual Themes/Controls/{Name}.xaml files —
they must be in the runtime-merged token dictionary to resolve.
The ~67 skins & their tokens
Each entry of the BeepControlStyle enum maps 1:1 to a
Themes/Styles/{name}.xaml file — roughly 67 skins spanning design systems from
Material3, Fluent2, iOS15 and MacOSBigSur through Apple, Dracula, Nord, Cyberpunk, Tokyo,
Solarized, OneDark, GruvBox and Claymorphism. BeepChrome.GetDesignSystemName is the
enum → filename map.
Every skin declares the same semantic token set, keyed
{Skin}{Token}, from which its per-control styles paint directly. There are 12
color tokens and a small set of geometry tokens. From Dracula.xaml:
<!-- 12 color tokens -->
<SolidColorBrush x:Key="DraculaSurface" Color="#282A36"/>
<SolidColorBrush x:Key="DraculaOnSurface" Color="#F8F8F2"/>
<SolidColorBrush x:Key="DraculaOnSurfaceVariant" Color="#CFCFEA"/>
<SolidColorBrush x:Key="DraculaAccent" Color="#BD93F9"/>
<SolidColorBrush x:Key="DraculaBorder" Color="#44475A"/>
<SolidColorBrush x:Key="DraculaHoverSurface" Color="#3E3A53"/>
<SolidColorBrush x:Key="DraculaPressedSurface" Color="#4F4569"/>
<SolidColorBrush x:Key="DraculaDisabled" Color="#7F8185"/>
<SolidColorBrush x:Key="DraculaError" Color="#FF5555"/>
<SolidColorBrush x:Key="DraculaWarning" Color="#F1FA8C"/>
<SolidColorBrush x:Key="DraculaSuccess" Color="#50FA7B"/>
<SolidColorBrush x:Key="DraculaInfo" Color="#8BE9FD"/>
<!-- geometry tokens -->
<CornerRadius x:Key="DraculaControlRadius">8</CornerRadius>
<Thickness x:Key="DraculaControlBorder">1</Thickness>
<Thickness x:Key="DraculaControlPad">12,8,12,8</Thickness>
| Token | Role |
|---|---|
{Skin}Surface | Base control background. |
{Skin}OnSurface | Primary text/icon on the surface. |
{Skin}OnSurfaceVariant | Muted / secondary text (OnSurface blended ~40% toward Surface). |
{Skin}Accent | Primary accent (selection, focus, buttons). |
{Skin}Border | Control border color. |
{Skin}HoverSurface | Surface tinted toward Accent for hover. |
{Skin}PressedSurface | Deeper Accent tint for pressed. |
{Skin}Disabled | Disabled foreground/surface. |
{Skin}Error / {Skin}Warning / {Skin}Success / {Skin}Info | Semantic status colors. |
Plus geometry tokens — {Skin}ControlRadius, {Skin}ControlBorder,
{Skin}ControlPad (and a font-size token) — so a control's shape and spacing are
per-skin too. A skin's keyed control styles bind these {Skin}* resources
directly with StaticResource; that is why "the skins are the
theme." BeepChrome.FindSkinBrush resolves a semantic name (e.g. "Error") against
the active skin ({Skin}Error) for code that needs a palette brush at runtime.
WpfThemesManager — the single skin authority
WpfThemesManager (namespace
TheTechIdea.Beep.WPF.Controls.ThemeManagement) is the one entry point for
switching skin app-wide — the WPF counterpart of the WinForms BeepThemesManager.
Route every skin change through it.
| Member | What it does |
|---|---|
CurrentStyle | The active skin (BeepControlStyle). Single source of truth; defaults to Material3. |
CurrentThemeName | The active theme (palette) name; defaults to "Light". |
ApplyStyle(style) | The one call to switch skin. Stamps BeepChrome.ControlStyle on every open window (inherits → cascades down each tree; the first stamp merges the skin RD so DynamicResources re-resolve), then raises StyleChanged. |
StyleChanged | Event raised after the skin changes — for views/dialogs/popups in separate visual trees, or created later, to re-stamp themselves. |
ThemeChanged / RaiseThemeChanged(name) | Palette-name change notification. |
// Switch skin for the whole app
WpfThemesManager.ApplyStyle(BeepControlStyle.Material3);
// A dialog/popup/toast in a SEPARATE visual tree must stamp itself at creation —
// ApplyStyle only stamps windows already open, and separate trees don't inherit.
BeepChrome.SetControlStyle(root, WpfThemesManager.CurrentStyle);
Window, dialog, toast or
Popup with generated content does not inherit ControlStyle from the
main window. Stamp it with WpfThemesManager.CurrentStyle at creation, or subscribe
to StyleChanged and re-stamp. Generic.xaml's implicit styles are the
frozen startup default (Material3), not a runtime fallback for elements alive after a
skin switch.
Known issue: never assign a *Brush to a SolidColorBrush.Color
SolidColorBrush.Color property takes a Color, not a
Brush. Assigning a *Brush resource to it — or binding a Brush's
Color onto a resource SolidColorBrush — silently fails: the
Color stays its default and the brush paints black.
<!-- BROKEN — BeepOnSurfaceBrush is a Brush, not a Color → renders black -->
<SolidColorBrush x:Key="BeepStyleOnSurfaceBrush" Color="{DynamicResource BeepOnSurfaceBrush}" />
<!-- ALSO BROKEN — a Binding onto a resource Freezable's Color doesn't establish -->
<SolidColorBrush x:Key="BeepStyleOnSurfaceBrush"
Color="{Binding Color, Source={StaticResource TerminalOnSurface}}" />
<!-- CORRECT — a literal hex Color, or a Color (not Brush) resource -->
<SolidColorBrush x:Key="BeepStyleOnSurfaceBrush" Color="#1A1A1A" />
This defect made the shared BeepStyle*Brush tokens render black, which cascaded to
every consumer resolving color through them — most visibly the BeepListItem
fallback variant, so BeepComboBox drop-downs and BeepTreeView nodes
showed black text. Controls binding {StaticResource {Skin}OnSurface} directly were
unaffected. The rule: in Themes/, a SolidColorBrush.Color must be a
literal hex or a Color resource — never a *Brush, and never a
Binding to one. See
_plan/BEEPSTYLE_COLOR_PIPELINE_FIX_PLAN.md for the full analysis and remediation
(Phases A–D).