Skip to content

Themes and Skins Migration Strategy

ASP.NET Web Forms Themes and Skins provided a centralized way to control the visual appearance of server controls across an entire application. Blazor has no built-in equivalent, but several Blazor-native approaches can achieve the same goals. This document explains the original Web Forms features, evaluates Blazor alternatives, and recommends a migration path.

Current Status — PoC Implemented (M10)

The theming/skinning system is now implemented as a proof-of-concept. SkinID (typed as string) and EnableTheming (typed as bool, defaults to true) are active [Parameter] properties on BaseWebFormsComponent — they are not [Obsolete]. The ThemeProvider component, ThemeConfiguration, and ControlSkin classes are live in the BlazorWebFormsComponents.Theming namespace. See the PoC Decisions section below for design choices.


What Are Web Forms Themes and Skins?

Themes

A Theme is a named collection of CSS files, skin files (.skin), and images stored in a well-known folder structure:

App_Themes/
└── Professional/
    ├── styles.css
    ├── controls.skin
    └── Images/
        └── logo.png

Themes are applied in three ways:

Method Scope Example
web.config Application-wide <pages theme="Professional" />
@Page directive Single page <%@ Page Theme="Professional" %>
Programmatic Dynamic Page.Theme = "Professional" in Page_PreInit

Themes have two modes that control precedence:

  • Theme property — the theme overrides control property settings (theme wins over markup)
  • StyleSheetTheme property — the theme acts as defaults (markup wins over theme)

This override-vs-default distinction is critical for any migration approach.

Skins (.skin Files)

A Skin file is an XML-like file containing ASP.NET server control declarations with appearance property values — but no ID attribute and no behavioral properties:

<!-- Default skin — applies to ALL Button instances -->
<asp:Button runat="server"
    BackColor="#C04000"
    BorderColor="Maroon"
    Font-Names="Tahoma"
    Font-Size="10pt" />

<!-- Named skin — applies only when SkinID="action" -->
<asp:Button runat="server" SkinID="action"
    BackColor="Navy"
    ForeColor="White"
    Font-Bold="True" />

Key rules:

  • Default skins (no SkinID) apply to every instance of that control type automatically
  • Named skins (SkinID="action") apply only to controls with a matching SkinID property
  • Only one default skin per control type is allowed
  • Only appearance properties belong in skins (colors, fonts, borders — not Text, CommandName, etc.)

SkinID Property

Every Web Forms control inherits SkinID from System.Web.UI.Control:

<!-- Uses the default Button skin -->
<asp:Button ID="btnCancel" runat="server" Text="Cancel" />

<!-- Uses the "action" named skin -->
<asp:Button ID="btnSave" runat="server" Text="Save" SkinID="action" />

Implemented

SkinID is correctly typed as string on BaseWebFormsComponent and defaults to "" (empty string), which selects the default skin. Set SkinID="action" to select a named skin — just like Web Forms.


Blazor CSS Capabilities

Before evaluating approaches, here are the Blazor features available as building blocks:

Feature Description
CSS Isolation {Component}.razor.css files with auto-scoped selectors via b-{hash} attributes
::deep Pseudo-element for styling child component markup from a parent's scoped CSS
CSS Custom Properties Native CSS variables (var(--color-primary)) for theming
CascadingValue / CascadingParameter Pass values down the component tree without explicit parameters
DI Services Inject configuration objects via dependency injection
RCL Bundling Component library CSS bundled as {PackageId}.bundle.scp.css

Approach 1: CSS Custom Properties (Variables)

Define theme variables at the root or page level; components reference them with var().

How It Works

Define the theme (CSS file or <style> block):

/* themes/professional.css */
:root {
    --button-bg: #C04000;
    --button-border: Maroon;
    --button-font: "Tahoma", sans-serif;
    --button-font-size: 10pt;

    --button-action-bg: Navy;
    --button-action-fg: White;
    --button-action-font-weight: bold;
}

Component CSS uses the variables:

/* Button.razor.css */
button {
    background-color: var(--button-bg, #ddd);
    border-color: var(--button-border, #ccc);
    font-family: var(--button-font, inherit);
    font-size: var(--button-font-size, inherit);
}

SkinID support via CSS classes:

/* Named skin: action */
button.skin-action {
    background-color: var(--button-action-bg, Navy);
    color: var(--button-action-fg, White);
    font-weight: var(--button-action-font-weight, bold);
}
<!-- Component applies SkinID as a CSS class -->
<Button Text="Save" SkinID="action" />
<!-- Renders: <button class="skin-action">Save</button> -->

Theme switching — swap the CSS file:

<!-- In App.razor or a layout -->
<link href="themes/@currentTheme.css" rel="stylesheet" />

Theme vs StyleSheetTheme Semantics

Mode CSS Approach
Theme (override) Variables defined with higher specificity or !important
StyleSheetTheme (default) Variables defined on :root with normal specificity; inline styles from markup win naturally

CSS custom properties with var(--prop, fallback) already behave like StyleSheetTheme — an inline style attribute set in markup wins over the variable. Theme (override) semantics are harder; they require !important or higher-specificity selectors, which is fragile.

Strengths

  • ✅ Pure CSS — no C# runtime cost
  • ✅ Theme switching is instant (swap a stylesheet)
  • ✅ Browser DevTools support for live editing
  • ✅ Works with CSS isolation and ::deep
  • ✅ Incrementally adoptable — add variables one component at a time

Weaknesses

  • ❌ Only works for CSS-expressible properties (colors, fonts, borders)
  • ❌ Cannot set non-CSS properties (Text, Width as an HTML attribute, Visible)
  • Theme override semantics require !important hacks
  • ❌ SkinID → CSS class mapping must be built into each component
  • ❌ No compile-time validation of variable names

Approach 2: CascadingValue ThemeProvider

A wrapper component provides default property values via CascadingParameter.

How It Works

Define a theme model (actual implementation):

// BlazorWebFormsComponents.Theming.ThemeConfiguration
public class ThemeConfiguration
{
    public void AddSkin(string controlTypeName, ControlSkin skin, string skinId = null);
    public ControlSkin GetSkin(string controlTypeName, string skinId = null);
    public bool HasSkins(string controlTypeName);
}

// BlazorWebFormsComponents.Theming.ControlSkin
public class ControlSkin
{
    public WebColor BackColor { get; set; }
    public WebColor ForeColor { get; set; }
    public WebColor BorderColor { get; set; }
    public BorderStyle? BorderStyle { get; set; }
    public Unit? BorderWidth { get; set; }
    public string CssClass { get; set; }
    public Unit? Height { get; set; }
    public Unit? Width { get; set; }
    public FontInfo Font { get; set; }
    public string ToolTip { get; set; }
}

The ThemeProvider component (actual implementation):

<!-- ThemeProvider.razor (in BlazorWebFormsComponents.Theming) -->
<CascadingValue Value="Theme">
    @ChildContent
</CascadingValue>

@code {
    [Parameter] public ThemeConfiguration Theme { get; set; }
    [Parameter] public RenderFragment ChildContent { get; set; }
}

Wrap your app or page:

<ThemeProvider Theme="@professionalTheme">
    <Router AppAssembly="@typeof(App).Assembly">
        <!-- ... -->
    </Router>
</ThemeProvider>

@code {
    private ThemeConfiguration professionalTheme = new ThemeConfiguration();

    protected override void OnInitialized()
    {
        professionalTheme.AddSkin("Button", new ControlSkin
        {
            BackColor = WebColor.Blue,
            ForeColor = WebColor.White
        });
    }
}

Components read from the cascaded theme (actual implementation in BaseStyledComponent):

// Inside BaseStyledComponent
[CascadingParameter]
public ThemeConfiguration Theme { get; set; }

protected override void OnParametersSet()
{
    base.OnParametersSet();

    if (!EnableTheming || Theme == null) return;

    var skin = Theme.GetSkin(GetType().Name, SkinID);
    if (skin == null) return;

    ApplySkin(skin);
}

private void ApplySkin(ControlSkin skin)
{
    // StyleSheetTheme semantics: apply only if property not explicitly set
    if (BackColor == default && skin.BackColor != default)
        BackColor = skin.BackColor;

    if (ForeColor == default && skin.ForeColor != default)
        ForeColor = skin.ForeColor;

    // ... repeat for other appearance properties
}

Theme vs StyleSheetTheme Semantics

// The current PoC implements StyleSheetTheme semantics only:
// theme sets defaults, but explicit component values take precedence.
// Theme (override) mode is deferred to M11.

// In BaseStyledComponent.ApplySkin:
// Only apply if the component property is at its default value
if (BackColor == default && skin.BackColor != default)
    BackColor = skin.BackColor;

The current implementation uses StyleSheetTheme semantics exclusively — the theme sets defaults, and any value explicitly set on the component takes precedence. Theme (override) mode, where the theme always wins, is deferred to M11.

SkinID Support

<!-- Default skin applies automatically -->
<Button Text="Cancel" />

<!-- Named skin "action" applies -->
<Button Text="Save" SkinID="action" />

Both work identically to Web Forms — the base class resolves the correct skin entry.

Strengths

  • ✅ Faithful to Web Forms semantics — supports both Theme and StyleSheetTheme modes
  • ✅ SkinID works exactly like Web Forms
  • ✅ Can set any property, not just CSS-related ones
  • ✅ Compile-time type safety on the theme model
  • ✅ Theme data could be loaded from .skin-like configuration files
  • ✅ Single point of change in BaseWebFormsComponent benefits all 50+ components

Weaknesses

  • ❌ Requires changes to BaseWebFormsComponent (touches every component)
  • ❌ Runtime cost — theme resolution on every OnParametersSet
  • ❌ Not a standard Blazor pattern — new contributors must learn it
  • ❌ Theme switching requires re-render of the entire component tree

Approach 3: Generated CSS Isolation Files

Pre-generate .razor.css files for each component from skin definitions. Closest to Jeff's initial idea.

How It Works

Source: a skin definition file (JSON, YAML, or .skin):

{
    "theme": "Professional",
    "skins": {
        "Button": {
            "default": {
                "background-color": "#C04000",
                "border-color": "Maroon",
                "font-family": "Tahoma",
                "font-size": "10pt"
            },
            "action": {
                "background-color": "Navy",
                "color": "White",
                "font-weight": "bold"
            }
        },
        "TextBox": {
            "default": {
                "border": "1px solid #999",
                "padding": "4px"
            }
        }
    }
}

Build-time tool generates scoped CSS:

/* Button.razor.css (generated) */
button {
    background-color: #C04000;
    border-color: Maroon;
    font-family: "Tahoma", sans-serif;
    font-size: 10pt;
}

button[data-skinid="action"] {
    background-color: Navy;
    color: White;
    font-weight: bold;
}

SkinID rendered as a data- attribute:

<!-- Button.razor (modified) -->
<button data-skinid="@SkinID" class="@CssClass" style="@Style">
    @Text
</button>

Theme Switching

Generate separate CSS bundles per theme. Switch by loading a different bundle:

<link href="_content/BlazorWebFormsComponents/themes/professional.css" rel="stylesheet" />

Theme vs StyleSheetTheme Semantics

Mode Generated CSS Approach
StyleSheetTheme (default) Generated CSS uses low specificity; inline style attributes from markup override
Theme (override) Generated CSS uses !important or high-specificity selectors

Strengths

  • ✅ Aligns with Jeff's initial idea
  • ✅ Pure CSS output — no runtime cost
  • ✅ Leverages Blazor's built-in CSS isolation
  • ✅ Theme files can be authored by designers without C# knowledge
  • ✅ Build-time validation of skin definitions

Weaknesses

  • ❌ Requires a build-time code generation tool (MSBuild task or Source Generator)
  • ❌ Only works for CSS-expressible properties
  • ❌ Cannot set non-CSS properties (Visible, Width as attribute, etc.)
  • ❌ Generated CSS fights with CSS isolation scoping (b-{hash} attributes)
  • ❌ Theme override semantics are fragile (!important)
  • ❌ Significant tooling investment for a migration-only feature

Approach 4: Dictionary-Based Configuration via DI

A ThemeConfiguration service registered in DI holds property defaults per control type, keyed by optional SkinID. (Note: This section shows a hypothetical DI-based variant — not the actual implementation. See Approach 2 for the implemented API.)

How It Works

Define the configuration service:

public class ThemeConfiguration
{
    public string ThemeName { get; set; } = "Default";
    public ThemeMode Mode { get; set; } = ThemeMode.StyleSheetTheme;

    private readonly Dictionary<string, ControlSkinDefaults> _skins = new();

    /// <summary>
    /// Register a default skin for a control type.
    /// </summary>
    public void AddDefaultSkin<TControl>(ControlSkinDefaults defaults)
        where TControl : BaseWebFormsComponent
    {
        _skins[typeof(TControl).Name] = defaults;
    }

    /// <summary>
    /// Register a named skin for a control type.
    /// </summary>
    public void AddNamedSkin<TControl>(string skinId, ControlSkinDefaults defaults)
        where TControl : BaseWebFormsComponent
    {
        _skins[$"{typeof(TControl).Name}:{skinId}"] = defaults;
    }

    public ControlSkinDefaults? GetSkin(Type controlType, string? skinId = null)
    {
        if (!string.IsNullOrEmpty(skinId) &&
            _skins.TryGetValue($"{controlType.Name}:{skinId}", out var named))
            return named;

        _skins.TryGetValue(controlType.Name, out var def);
        return def;
    }
}

Register in DI:

// Program.cs
builder.Services.AddSingleton<ThemeConfiguration>(sp =>
{
    var theme = new ThemeConfiguration { ThemeName = "Professional" };

    theme.AddDefaultSkin<Button>(new ControlSkinDefaults
    {
        BackColor = WebColor.FromName("Maroon"),
        FontFamily = "Tahoma",
        FontSize = "10pt"
    });

    theme.AddNamedSkin<Button>("action", new ControlSkinDefaults
    {
        BackColor = WebColor.FromName("Navy"),
        ForeColor = WebColor.FromName("White"),
        FontBold = true
    });

    return theme;
});

Components inject the service:

// BaseWebFormsComponent.cs
[Inject]
protected ThemeConfiguration? ThemeConfig { get; set; }

protected override void OnParametersSet()
{
    if (ThemeConfig != null)
    {
        var skin = ThemeConfig.GetSkin(GetType(), SkinID);
        if (skin != null) ApplySkin(skin, ThemeConfig.Mode);
    }
}

Theme Switching

Swap the DI registration or use a scoped service:

builder.Services.AddScoped<ThemeConfiguration>(sp =>
    ThemeLoader.LoadFromJson("wwwroot/themes/professional.json"));

Strengths

  • ✅ Standard DI pattern — familiar to Blazor developers
  • ✅ Supports both Theme and StyleSheetTheme semantics
  • ✅ SkinID works like Web Forms
  • ✅ Can set any property (not limited to CSS)
  • ✅ Theme data can be loaded from JSON/XML files at startup
  • ✅ No component tree re-render on theme switch (if scoped per-circuit)

Weaknesses

  • ❌ Service injection in base component may surprise library consumers
  • ❌ Configuration is imperative (code), not declarative (markup)
  • ❌ Requires changes to BaseWebFormsComponent
  • ❌ No cascading scope — the theme applies globally per DI scope
  • ❌ Cannot easily scope a theme to a subtree of components (unlike CascadingValue)

Approach 5: Hybrid — CSS Variables + CascadingParameter

Use CSS custom properties for visual styles (colors, fonts, borders) and CascadingParameter for non-CSS properties.

How It Works

CSS variables handle the visual layer:

/* themes/professional.css */
:root {
    --bwfc-button-bg: #C04000;
    --bwfc-button-border: Maroon;
    --bwfc-button-font: "Tahoma", sans-serif;
}

CascadingParameter handles non-CSS properties:

public class ThemeDefaults
{
    public ThemeMode Mode { get; set; } = ThemeMode.StyleSheetTheme;
    public Dictionary<(Type, string?), NonCssDefaults> Defaults { get; } = new();
}

public class NonCssDefaults
{
    public string? Width { get; set; }   // HTML attribute, not CSS
    public bool? Visible { get; set; }
    public string? ToolTip { get; set; }
}

Components use both:

/* Button.razor.css */
button {
    background-color: var(--bwfc-button-bg);
    border-color: var(--bwfc-button-border);
    font-family: var(--bwfc-button-font);
}
// Button.razor.cs
[CascadingParameter(Name = "ThemeDefaults")]
private ThemeDefaults? ThemeDefaults { get; set; }

protected override void OnParametersSet()
{
    // CSS properties handled by CSS variables — no C# needed
    // Only non-CSS properties resolved here
    if (ThemeDefaults?.Defaults.TryGetValue(
            (GetType(), SkinID), out var defaults) == true)
    {
        if (Width == null && defaults.Width != null)
            Width = defaults.Width;
    }
}

SkinID maps to CSS class + cascading lookup:

button.skin-action {
    background-color: var(--bwfc-button-action-bg, Navy);
    color: var(--bwfc-button-action-fg, White);
}

Strengths

  • ✅ Best of both worlds — CSS for visual, C# for structural
  • ✅ Theme switching is fast (CSS swap for visuals, no full re-render)
  • ✅ Clean separation of concerns
  • ✅ Incrementally adoptable

Weaknesses

  • ❌ Two systems to learn and maintain
  • ❌ Splitting properties between CSS and C# creates confusion
  • ❌ SkinID must be handled in both CSS and C# code
  • ❌ Most complex implementation of all approaches

Comparison Matrix

Criteria CSS Variables CascadingValue Generated CSS DI Service Hybrid
Web Forms SkinID fidelity ⚠️ Partial ✅ Full ⚠️ Partial ✅ Full ✅ Full
Theme mode (override) ❌ Fragile ✅ Yes ❌ Fragile ✅ Yes ⚠️ Partial
StyleSheetTheme mode (default) ✅ Natural ✅ Yes ✅ Natural ✅ Yes ✅ Yes
Non-CSS properties ❌ No ✅ Yes ❌ No ✅ Yes ✅ Yes
Runtime performance ✅ Zero ⚠️ Per-render ✅ Zero ⚠️ Per-render ⚠️ Mixed
Incremental adoption ✅ Easy ⚠️ Medium ❌ Hard ⚠️ Medium ⚠️ Medium
Tooling investment ✅ None ✅ Low ❌ High ✅ Low ⚠️ Medium
Scoped to subtree ✅ CSS scope ✅ Cascading ✅ CSS scope ❌ Global DI ✅ Mixed
Blazor-idiomatic ✅ Yes ✅ Yes ⚠️ Somewhat ✅ Yes ⚠️ Complex

Implemented Approach: CascadingValue ThemeProvider (Approach 2)

The CascadingValue ThemeProvider is the selected and implemented approach. It was chosen for the following reasons:

Rationale

  1. Highest Web Forms fidelity. It is the only approach that can faithfully model both Theme (override) and StyleSheetTheme (default) semantics, plus SkinID selection, using the same mental model as the original. The PoC defaults to StyleSheetTheme semantics (theme sets defaults, explicit values override).

  2. Works for all property types. Unlike CSS-only approaches, it sets BackColor, ForeColor, Width, ToolTip, CssClass, Font, and other appearance properties — matching what .skin files actually do. The ControlSkin class mirrors the properties on BaseStyledComponent.

  3. Single point of change. The theme resolution logic lives in BaseStyledComponent.OnParametersSet. Once implemented there, all 50+ components in the library inherit the behavior automatically. No per-component work required.

  4. Familiar Blazor pattern. CascadingValue/CascadingParameter is a well-documented, first-class Blazor feature. Developers already encounter it with EditContext, CascadingAuthenticationState, and the library's own TableItemStyle cascading.

  5. Scoped application. Unlike DI (which is global), a CascadingValue can be scoped to a page or section — matching how Web Forms allows StyleSheetTheme on a per-page basis via the @Page directive.

  6. Incrementally adoptable. The ThemeProvider component and base class changes ship without requiring any existing consumer to change their code. Themes are opt-in: if no ThemeProvider wraps the component tree, behavior is unchanged.

Implementation Roadmap

Phase 1 — Foundation (✅ Complete — M10 PoC)

  • ~~Fix SkinID type from bool to string on BaseWebFormsComponent~~ ✅ Done
  • ~~Remove [Obsolete] from SkinID and EnableTheming~~ ✅ Done
  • ~~Define ControlSkin and ThemeConfiguration~~ ✅ Done (in BlazorWebFormsComponents.Theming namespace)
  • ~~Create ThemeProvider cascading wrapper component~~ ✅ Done
  • ~~Add theme resolution to BaseStyledComponent.OnParametersSet~~ ✅ Done

Phase 2 — Full Implementation (Deferred to M11)

  • .skin file parser (reading actual .skin files)
  • StyleSheetTheme vs Theme priority distinction
  • Runtime theme switching
  • Sub-component style theming (HeaderStyle, RowStyle, etc.)
  • Container-level EnableTheming propagation to children
  • JSON theme format as alternative input

Phase 3 — Documentation & Samples

  • Sample page demonstrating theme switching
  • Migration guide addendum showing before/after for themed Web Forms apps

Complementary CSS Variables

While the CascadingValue approach is primary, nothing prevents developers from also using CSS custom properties for pure-CSS theming on top. The two are complementary:

<!-- CascadingValue for property defaults -->
<ThemeProvider Theme="@myTheme">
    <!-- CSS variables for visual layer -->
    <div style="--bwfc-primary: Navy; --bwfc-accent: Gold;">
        <Button Text="Themed" SkinID="action" />
    </div>
</ThemeProvider>

Migration Example: Before and After

Before — Web Forms with Theme

App_Themes/Corporate/buttons.skin:

<asp:Button runat="server"
    BackColor="#336699"
    ForeColor="White"
    Font-Names="Segoe UI"
    Font-Size="9pt"
    BorderStyle="None" />

<asp:Button runat="server" SkinID="danger"
    BackColor="#CC3333"
    ForeColor="White"
    Font-Bold="True" />

web.config:

<pages theme="Corporate" />

Products.aspx:

<%@ Page Title="Products" %>
<asp:Button ID="btnSave" runat="server" Text="Save" />
<asp:Button ID="btnDelete" runat="server" Text="Delete" SkinID="danger" />

After — Blazor with ThemeProvider

CorporateTheme.cs:

using BlazorWebFormsComponents.Theming;

public static class CorporateTheme
{
    public static ThemeConfiguration Create()
    {
        var theme = new ThemeConfiguration();

        // Default skin — applies to ALL Button instances
        theme.AddSkin("Button", new ControlSkin
        {
            BackColor = WebColor.FromHtml("#336699"),
            ForeColor = WebColor.FromName("White"),
            Font = new FontInfo { Name = "Segoe UI", Size = new FontUnit("9pt") },
        });

        // Named skin — applies only when SkinID="danger"
        theme.AddSkin("Button", new ControlSkin
        {
            BackColor = WebColor.FromHtml("#CC3333"),
            ForeColor = WebColor.FromName("White"),
            Font = new FontInfo { Bold = true }
        }, "danger");

        return theme;
    }
}

App.razor (or layout):

@using BlazorWebFormsComponents.Theming

<ThemeProvider Theme="@CorporateTheme.Create()">
    <Router AppAssembly="@typeof(App).Assembly">
        <Found Context="routeData">
            <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
        </Found>
    </Router>
</ThemeProvider>

Products.razor:

@page "/products"

<Button ID="btnSave" Text="Save" />
<Button ID="btnDelete" Text="Delete" SkinID="danger" />

The markup is nearly identical. The theme is applied automatically, just like Web Forms.


PoC Decisions

The following design decisions were made during the M10 PoC implementation:

Decision Rationale
Default to StyleSheetTheme semantics The theme sets default values; any value explicitly set on a component takes precedence. This is the safer default for migration — existing markup is preserved. Theme (override) mode is deferred to M11.
Missing SkinID: log warning, don't throw When a component specifies SkinID="foo" but no skin named "foo" is registered, GetSkin returns null and the component renders without theme styling. This avoids breaking the app due to configuration mismatches.
Namespace: BlazorWebFormsComponents.Theming All theme-related types (ThemeConfiguration, ControlSkin, ThemeProvider) live in a dedicated Theming namespace to keep the root namespace clean.
ThemeConfiguration keyed by string, not Type Control type names are stored as strings (e.g., "Button") with case-insensitive comparison. This avoids tight coupling to concrete types and simplifies the API.
ControlSkin mirrors BaseStyledComponent properties The skin model uses the same property types (WebColor, Unit, FontInfo, etc.) as the component base class, ensuring type-safe assignment.
Theme resolution in BaseStyledComponent, not BaseWebFormsComponent Only styled components can be themed — the [CascadingParameter] ThemeConfiguration Theme and ApplySkin logic live in BaseStyledComponent.
.skin file parsing deferred to M11 Parsing legacy .skin files requires a dedicated parser for the pseudo-ASPX format. The PoC validates the C# configuration API first; .skin file support will layer on top in M11.

Additional Resources