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.

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

TokenRoleLight Theme DefaultDark Theme Default
PrimaryBrand color, primary actions, active states #6750A4 #D0BCFF
OnPrimaryText/icons on primary-colored surfaces #FFFFFF #381E72
PrimaryContainerProminent container for key UI elements #EADDFF #4F378B
SurfaceDefault background of components #FFFBFE #1C1B1F
OnSurfacePrimary text and iconography on surfaces #1C1B1F #E6E1E5
SurfaceVariantAlternative surface for differentiation #E7E0EC #49454F
OutlineBorders, dividers, and structural lines #79747E #938F99
ErrorValidation 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:

StateOpacity OverlayUse Case
Hover8%Mouse cursor over component
Focus12%Keyboard focus ring
Pressed12%Mouse button down
Dragged16%Active drag operation

Built-in Themes

ThemeManager ships five ready-made SkiaTheme presets. Each defines the full Material Design 3.0 token set:

ThemeCharacterBest For
LightPurple-seeded light scheme (default)General purpose, print-adjacent work
DarkLow-light scheme with desaturated containersDark-mode-first applications
HighContrastMaximum-contrast foreground/background pairsAccessibility, presentations, projectors
NordCool blue-gray paletteCalm, technical diagrams
DraculaVibrant dark paletteDeveloper 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:

StyleVisualPaint ConfigurationUses
NoneNo borderNo strokeFlat components, background elements
Single─────Single solid stroke using the theme outline colorStandard component borders
Fixed3DRaised 3D effectLight top/left + dark bottom/right strokesButtons, toolbars, raised panels
FixedSingleSunken single lineInset single strokeText 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

RoleSizeWeightLetter SpacingUse Case
Display57px400-0.25pxHero headlines, diagram titles
Headline28px4000pxSection headers, family names
Title22px5000pxComponent titles, node labels
Body16px4000.5pxComponent body text, descriptions
Label14px5000.1pxConnection labels, port names
Caption12px4000.4pxMetadata, 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

ValueDescriptionTypical Usage
SKTextAlign.LeftText starts at the anchorProperty rows, left-aligned labels
SKTextAlign.CenterText centers on the anchorNode labels (default)
SKTextAlign.RightText ends at the anchorNumeric 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

LevelOffset YBlurOpacityUse Case
00px0px0%Flat components: labels, text nodes
11px3px12%Standard nodes, buttons, inputs
22px6px16%Cards, panels, selected components
34px8px19%Floating toolbars, modal dialogs
46px12px24%Menus, dropdowns, popovers
58px16px28%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();
}