Theming Guide
Material Design 3.0 theming, custom color tokens, and visual customization
Beep.Skia implements a comprehensive Material Design 3.0 theming system modeled after the Material You design language. Every visual component — from diagram nodes to UI controls — resolves its colors through a centralized token system that supports light, dark, and custom themes.
Table of Contents
Material Design 3.0 Tokens
The theme system is built on a set of named color tokens that propagate through the component tree. Each token represents a semantic role in the UI, not a specific hex color. Tokens are resolved at render time based on the current theme, dark/light mode, and component-level overrides.
Core Color Tokens
| Token | Role | Light Theme Default | Dark Theme Default |
|---|---|---|---|
Primary | Brand color, primary actions, active states | #6750A4 | #D0BCFF |
OnPrimary | Text/icons on primary-colored surfaces | #FFFFFF | #381E72 |
PrimaryContainer | Prominent container for key UI elements | #EADDFF | #4F378B |
Surface | Default background of components | #FFFBFE | #1C1B1F |
OnSurface | Primary text and iconography on surfaces | #1C1B1F | #E6E1E5 |
SurfaceVariant | Alternative surface for differentiation | #E7E0EC | #49454F |
Outline | Borders, dividers, and structural lines | #79747E | #938F99 |
Error | Validation errors, destructive actions | #B3261E | #F2B8B5 |
Token Propagation
Tokens come from a single active SkiaTheme exposed through the static ThemeManager. Every MaterialControl reads the active theme when it draws, so changing the theme affects the whole canvas on the next frame:
Token Resolution
// The active theme is a static, application-wide SkiaTheme
var theme = ThemeManager.Current;
var primary = theme.Primary;
var surface = theme.Surface;
var outline = theme.Outline;
// Components resolve tokens the same way while drawing
protected override void DrawContent(SKCanvas canvas, DrawingContext context)
{
var background = ThemeManager.Current.SurfaceContainer;
// ... draw using the active tokens
}
// Switching the theme raises ThemeChanged so hosts can invalidate
ThemeManager.ThemeChanged += (s, e) => drawingManager.RequestRedraw();
Because the theme is read at draw time, a single ThemeManager.ApplyTheme() call restyles every component — no per-component plumbing required.
State Layer Colors
Material Design 3.0 uses state layers for interactive feedback (hover, focus, pressed, dragged). MaterialControl exposes them as opacity constants and composites them over the base color:
| State | Opacity Overlay | Use Case |
|---|---|---|
| Hover | 8% | Mouse cursor over component |
| Focus | 12% | Keyboard focus ring |
| Pressed | 12% | Mouse button down |
| Dragged | 16% | Active drag operation |
Built-in Themes
ThemeManager ships five ready-made SkiaTheme presets. Each defines the full Material Design 3.0 token set:
| Theme | Character | Best For |
|---|---|---|
Light | Purple-seeded light scheme (default) | General purpose, print-adjacent work |
Dark | Low-light scheme with desaturated containers | Dark-mode-first applications |
HighContrast | Maximum-contrast foreground/background pairs | Accessibility, presentations, projectors |
Nord | Cool blue-gray palette | Calm, technical diagrams |
Dracula | Vibrant dark palette | Developer tooling, code-adjacent diagrams |
Applying a Built-in Theme
// Apply by name (Light, Dark, HighContrast, Nord, Dracula)
ThemeManager.ApplyTheme("Dark");
// Or assign a theme instance directly
ThemeManager.Current = SkiaTheme.HighContrast;
// Enumerate the built-in names
foreach (var name in ThemeManager.BuiltInThemes)
Console.WriteLine(name);
Static Color Tokens
MaterialDesignColors exposes the default Material Design 3.0 palette as static readonly SKColor fields. It is useful for fixed brand colors and for drawing custom content where no themed control is available:
Reading Static Tokens
using Beep.Skia.Components;
var primary = MaterialDesignColors.Primary;
var surface = MaterialDesignColors.Surface;
var outline = MaterialDesignColors.Outline;
var error = MaterialDesignColors.Error;
// The full token set is available:
// Primary/OnPrimary/PrimaryContainer/OnPrimaryContainer,
// Secondary/Tertiary families, Error family,
// Surface/OnSurface/SurfaceVariant/OnSurfaceVariant/SurfaceContainerHigh,
// Outline/OutlineVariant
Inside components, prefer ThemeManager.Current so drawing follows the active theme; use MaterialDesignColors when a color must stay fixed regardless of the theme.
Defining a Custom Color Scheme
var corporate = new SkiaTheme
{
Name = "Corporate",
Primary = new SKColor(0x00, 0x56, 0x9B), // Deep blue
OnPrimary = SKColors.White,
PrimaryContainer = new SKColor(0xD3, 0xE3, 0xFD),
Secondary = new SKColor(0x56, 0x8E, 0xB4),
Surface = new SKColor(0xFA, 0xFA, 0xFA),
OnSurface = new SKColor(0x1A, 0x1A, 0x1A),
SurfaceVariant = new SKColor(0xE8, 0xEE, 0xF4),
Outline = new SKColor(0x72, 0x7D, 0x8A),
Error = new SKColor(0xBA, 0x1A, 0x1A),
CanvasBackground = SKColors.White,
GridColor = new SKColor(0xE8, 0xE8, 0xE8)
};
ThemeManager.Current = corporate;
Custom Themes
Per-Component Color Overrides
Flowchart nodes can override their fill and stroke without touching the theme. Leave the properties null to inherit the theme's colors:
Component-Level Color Override
public class CriticalNode : ProcessNode
{
public CriticalNode()
{
Label = "Critical";
CustomFillColor = new SKColor(0xFF, 0xF0, 0xF0);
CustomStrokeColor = new SKColor(0xCC, 0x00, 0x00);
}
}
Per-Family Theme Overrides
To give a whole family a distinctive look, set the overrides in its base class so every derived node inherits them:
Family-Level Theming
public abstract class SecurityNodeBase : FlowchartControl
{
protected SecurityNodeBase()
{
// All security diagram nodes share a high-contrast palette
CustomFillColor = new SKColor(0x1A, 0x1A, 0x2E);
CustomStrokeColor = new SKColor(0x00, 0xFF, 0x88);
}
}
Dynamic Theme Switching
Switch themes at runtime by assigning ThemeManager.Current or calling ThemeManager.ApplyTheme(). Components read the theme while drawing, so a redraw is all that is needed:
Runtime Theme Toggle
public void ToggleDarkMode(bool dark)
{
ThemeManager.ApplyTheme(dark ? "Dark" : "Light");
drawingManager.RequestRedraw();
}
Theme Persistence
The active theme name is serialized with the diagram: DrawingManager.ToDto() writes DiagramDto.ThemeName and LoadFromDto() re-applies it through ThemeManager.ApplyTheme(), so a saved diagram reopens with the theme it was authored in.
Border Styles
Beep.Skia exposes the WinForms-style BorderStyle enumeration on themed controls, mapping each value to an SKPaint configuration:
| Style | Visual | Paint Configuration | Uses |
|---|---|---|---|
None | No border | No stroke | Flat components, background elements |
Single | ───── | Single solid stroke using the theme outline color | Standard component borders |
Fixed3D | Raised 3D effect | Light top/left + dark bottom/right strokes | Buttons, toolbars, raised panels |
FixedSingle | Sunken single line | Inset single stroke | Text fields, input areas |
Applying Border Styles
var container = new ProcessNode
{
Label = "Container",
X = 40, Y = 40, Width = 220, Height = 120
};
NodeProperties["BorderStyle"] = new ParameterInfo
{
ParameterName = "BorderStyle",
ParameterCurrentValue = BorderStyle.Single,
DefaultParameterValue = BorderStyle.Single,
ParameterType = typeof(BorderStyle),
Description = "Border style"
};
drawingManager.AddComponent(container);
Typography
The typography system is built on Material Design 3.0's type scale. Each role defines font family, size, weight, line height, and letter spacing. Components can reference roles directly or override individual properties.
Type Scale Roles
| Role | Size | Weight | Letter Spacing | Use Case |
|---|---|---|---|---|
Display | 57px | 400 | -0.25px | Hero headlines, diagram titles |
Headline | 28px | 400 | 0px | Section headers, family names |
Title | 22px | 500 | 0px | Component titles, node labels |
Body | 16px | 400 | 0.5px | Component body text, descriptions |
Label | 14px | 500 | 0.1px | Connection labels, port names |
Caption | 12px | 400 | 0.4px | Metadata, tooltips, small annotations |
Typography Configuration
public class TypedLabelComponent : MaterialControl
{
protected override void InitializeComponent()
{
base.InitializeComponent();
NodeProperties["FontFamily"] = new ParameterInfo
{
ParameterName = "FontFamily",
ParameterCurrentValue = "Inter",
DefaultParameterValue = "Inter",
ParameterType = typeof(string),
Description = "Font family"
};
NodeProperties["FontSize"] = new ParameterInfo
{
ParameterName = "FontSize",
ParameterCurrentValue = 14f,
DefaultParameterValue = 14f,
ParameterType = typeof(float),
Description = "Font size in points"
};
NodeProperties["FontWeight"] = new ParameterInfo
{
ParameterName = "FontWeight",
ParameterCurrentValue = SKFontStyleWeight.Normal,
DefaultParameterValue = SKFontStyleWeight.Normal,
ParameterType = typeof(SKFontStyleWeight),
Choices = new[] { "Light", "Normal", "Medium", "Bold" },
Description = "Font weight"
};
NodeProperties["TextAlign"] = new ParameterInfo
{
ParameterName = "TextAlign",
ParameterCurrentValue = SKTextAlign.Center,
DefaultParameterValue = SKTextAlign.Center,
ParameterType = typeof(SKTextAlign),
Choices = new[] { "Left", "Center", "Right" },
Description = "Text alignment"
};
}
}
Alignment Options
| Value | Description | Typical Usage |
|---|---|---|
SKTextAlign.Left | Text starts at the anchor | Property rows, left-aligned labels |
SKTextAlign.Center | Text centers on the anchor | Node labels (default) |
SKTextAlign.Right | Text ends at the anchor | Numeric values, right-aligned columns |
Shadows & Elevation
Material Design defines 6 elevation levels (0–5), each producing a different shadow rendering on MaterialControl subclasses. Shadows are rendered using SKImageFilter.CreateDropShadow() and cached per elevation level.
Elevation Levels
| Level | Offset Y | Blur | Opacity | Use Case |
|---|---|---|---|---|
| 0 | 0px | 0px | 0% | Flat components: labels, text nodes |
| 1 | 1px | 3px | 12% | Standard nodes, buttons, inputs |
| 2 | 2px | 6px | 16% | Cards, panels, selected components |
| 3 | 4px | 8px | 19% | Floating toolbars, modal dialogs |
| 4 | 6px | 12px | 24% | Menus, dropdowns, popovers |
| 5 | 8px | 16px | 28% | Dialogs, notifications, alerts |
Shadow Rendering
protected override void DrawContent(SKCanvas canvas, DrawingContext context)
{
if (Elevation > 0)
{
using var shadowFilter = SKImageFilter.CreateDropShadow(
0f, // dx
Elevation * 2f, // dy
Elevation * 2f, // sigmaX
Elevation * 2f, // sigmaY
new SKColor(0, 0, 0, (byte)(Elevation * 30)),
null);
using var shadowPaint = new SKPaint
{
IsAntialias = true,
ImageFilter = shadowFilter,
Style = SKPaintStyle.Fill,
Color = ThemeManager.Current.SurfaceContainer
};
canvas.DrawRoundRect(Bounds, 6f, 6f, shadowPaint);
}
// ... draw the component content on top ...
}
Performance Note
Drop shadows are computed on the CPU via SkiaSharp. On very large diagrams, set Elevation = 0 on the components that do not need depth; the default theme uses elevation 1 for most components, which balances visual depth and rendering cost.
Complete Theming Example: Custom Dark Theme Application
Applying a Complete Custom Theme
public void ApplyCorporateDarkTheme(DrawingManager drawingManager)
{
ThemeManager.Current = new SkiaTheme
{
Name = "Corporate Dark",
Primary = new SKColor(0x7B, 0xA4, 0xFF),
OnPrimary = new SKColor(0x00, 0x1A, 0x40),
PrimaryContainer = new SKColor(0x00, 0x2D, 0x6D),
Secondary = new SKColor(0x90, 0xCA, 0xF1),
Surface = new SKColor(0x0D, 0x0E, 0x13),
OnSurface = new SKColor(0xE2, 0xE2, 0xE9),
SurfaceVariant = new SKColor(0x2A, 0x2B, 0x33),
Outline = new SKColor(0x6B, 0x6D, 0x7B),
Error = new SKColor(0xFF, 0xB4, 0xAB),
CanvasBackground = new SKColor(0x0D, 0x0E, 0x13),
GridColor = new SKColor(0x2A, 0x2B, 0x33)
};
drawingManager.RequestRedraw();
}