Styling and Theming
Styling and theming are essential aspects of creating visually appealing and consistent user interfaces. Avalonia UI provides a powerful styling system that allows you to customize the appearance of controls and create cohesive themes for your applications.
Styles and Resources
Avalonia UI's styling system is inspired by CSS and allows you to define styles that target specific controls or control types.
Resources
Resources in Avalonia UI are objects that can be reused throughout your application. They are typically defined in resource dictionaries:
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="MyApp.MainWindow">
<Window.Resources>
<SolidColorBrush x:Key="PrimaryBrush" Color="#1E88E5" />
<SolidColorBrush x:Key="AccentBrush" Color="#FF4081" />
<SolidColorBrush x:Key="BackgroundBrush" Color="#F5F5F5" />
<SolidColorBrush x:Key="ForegroundBrush" Color="#212121" />
</Window.Resources>
<Button Background="{StaticResource PrimaryBrush}"
Foreground="White"
Content="Styled Button" />
</Window>
Resources can be defined at different levels:
- Control level: Available only to the control and its children
- Window level: Available to the entire window
- Application level: Available to the entire application
Application-level resources are defined in App.axaml
:
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="MyApp.App">
<Application.Resources>
<SolidColorBrush x:Key="PrimaryBrush" Color="#1E88E5" />
<SolidColorBrush x:Key="AccentBrush" Color="#FF4081" />
</Application.Resources>
</Application>
Resource Dictionaries
For better organization, you can define resources in separate resource dictionary files:
<!-- Themes/Colors.axaml -->
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Color x:Key="PrimaryColor">#1E88E5</Color>
<Color x:Key="AccentColor">#FF4081</Color>
<Color x:Key="BackgroundColor">#F5F5F5</Color>
<Color x:Key="ForegroundColor">#212121</Color>
<SolidColorBrush x:Key="PrimaryBrush" Color="{StaticResource PrimaryColor}" />
<SolidColorBrush x:Key="AccentBrush" Color="{StaticResource AccentColor}" />
<SolidColorBrush x:Key="BackgroundBrush" Color="{StaticResource BackgroundColor}" />
<SolidColorBrush x:Key="ForegroundBrush" Color="{StaticResource ForegroundColor}" />
</ResourceDictionary>
You can merge multiple resource dictionaries:
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="MyApp.App">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceInclude Source="/Themes/Colors.axaml" />
<ResourceInclude Source="/Themes/Fonts.axaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
Styles
Styles in Avalonia UI allow you to set properties on controls based on selectors. They are similar to CSS styles:
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="MyApp.MainWindow">
<Window.Styles>
<!-- Style targeting all Buttons -->
<Style Selector="Button">
<Setter Property="Background" Value="{StaticResource PrimaryBrush}" />
<Setter Property="Foreground" Value="White" />
<Setter Property="Padding" Value="10,5" />
<Setter Property="CornerRadius" Value="4" />
</Style>
<!-- Style targeting Buttons with class 'accent' -->
<Style Selector="Button.accent">
<Setter Property="Background" Value="{StaticResource AccentBrush}" />
</Style>
<!-- Style targeting TextBlocks inside StackPanels -->
<Style Selector="StackPanel > TextBlock">
<Setter Property="Margin" Value="0,5" />
</Style>
</Window.Styles>
<StackPanel Margin="20">
<TextBlock Text="Regular Button:" />
<Button Content="Regular Button" />
<TextBlock Text="Accent Button:" />
<Button Content="Accent Button" Classes="accent" />
</StackPanel>
</Window>
Style Selectors
Avalonia UI provides a powerful selector system for targeting controls:
- Type selector:
Button
targets all buttons - Name selector:
#myButton
targets the control with name "myButton" - Class selector:
.accent
targets controls with the "accent" class - Descendant selector:
StackPanel TextBlock
targets TextBlocks inside StackPanels - Child selector:
StackPanel > TextBlock
targets TextBlocks that are direct children of StackPanels - Property selector:
Button[IsDefault=true]
targets Buttons with IsDefault property set to true - Pseudo-class selector:
Button:pointerover
targets Buttons in the pointer-over state
You can combine selectors for more specific targeting:
<Style Selector="Button.accent:pointerover">
<Setter Property="Background" Value="{StaticResource AccentHoverBrush}" />
</Style>
Pseudo-classes
Pseudo-classes target controls in specific states:
:pointerover
- When the pointer is over the control:pressed
- When the control is pressed:disabled
- When the control is disabled:focus
- When the control has focus:selected
- When the control is selected (for selectable controls):checked
- When the control is checked (for checkable controls)
Example of using pseudo-classes:
<Style Selector="Button">
<Setter Property="Background" Value="{StaticResource PrimaryBrush}" />
<Setter Property="Foreground" Value="White" />
</Style>
<Style Selector="Button:pointerover">
<Setter Property="Background" Value="{StaticResource PrimaryHoverBrush}" />
</Style>
<Style Selector="Button:pressed">
<Setter Property="Background" Value="{StaticResource PrimaryPressedBrush}" />
</Style>
<Style Selector="Button:disabled">
<Setter Property="Opacity" Value="0.5" />
</Style>
Style Classes
You can apply style classes to controls to target them with class selectors:
<Button Content="Accent Button" Classes="accent" />
You can add or remove classes programmatically:
// Add a class
myButton.Classes.Add("accent");
// Remove a class
myButton.Classes.Remove("accent");
// Toggle a class
myButton.Classes.Toggle("accent");
// Check if a control has a class
bool hasAccentClass = myButton.Classes.Contains("accent");
Themes and Skins
Themes in Avalonia UI are collections of styles and resources that define the overall look and feel of your application.
Built-in Themes
Avalonia UI comes with two built-in themes:
- Fluent Theme: Based on Microsoft's Fluent Design System
- Simple Theme: A minimal theme with basic styling
To use the Fluent theme:
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="MyApp.App">
<Application.Styles>
<FluentTheme />
</Application.Styles>
</Application>
You can specify the theme variant (light or dark):
<FluentTheme Mode="Light" />
Or:
<FluentTheme Mode="Dark" />
Creating Custom Themes
You can create custom themes by defining styles and resources that override the default appearance of controls:
<!-- Themes/CustomTheme.axaml -->
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- Include a base theme -->
<FluentTheme />
<!-- Override styles -->
<Style Selector="Button">
<Setter Property="Background" Value="{StaticResource PrimaryBrush}" />
<Setter Property="Foreground" Value="White" />
<Setter Property="Padding" Value="15,8" />
<Setter Property="CornerRadius" Value="20" />
</Style>
<Style Selector="TextBox">
<Setter Property="BorderBrush" Value="{StaticResource PrimaryBrush}" />
<Setter Property="CornerRadius" Value="4" />
</Style>
<!-- More style overrides... -->
</Styles>
Apply the custom theme in your application:
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="MyApp.App">
<Application.Styles>
<StyleInclude Source="/Themes/CustomTheme.axaml" />
</Application.Styles>
</Application>
Theme Switching
You can implement theme switching to allow users to change the application theme at runtime:
public void SwitchTheme(bool isDark)
{
var app = Application.Current;
if (app is null) return;
// Remove existing theme
var existingTheme = app.Styles.OfType<FluentTheme>().FirstOrDefault();
if (existingTheme != null)
app.Styles.Remove(existingTheme);
// Add new theme
var newTheme = new FluentTheme
{
Mode = isDark ? FluentThemeMode.Dark : FluentThemeMode.Light
};
app.Styles.Add(newTheme);
}
Custom Controls
Sometimes, you need to create custom controls to implement specialized functionality or appearance.
Creating a Custom Control
To create a custom control, you typically inherit from an existing control class:
public class RatingControl : Control
{
public static readonly StyledProperty<int> ValueProperty =
AvaloniaProperty.Register<RatingControl, int>(nameof(Value), defaultValue: 0);
public static readonly StyledProperty<int> MaximumProperty =
AvaloniaProperty.Register<RatingControl, int>(nameof(Maximum), defaultValue: 5);
public int Value
{
get => GetValue(ValueProperty);
set => SetValue(ValueProperty, Math.Clamp(value, 0, Maximum));
}
public int Maximum
{
get => GetValue(MaximumProperty);
set => SetValue(MaximumProperty, Math.Max(1, value));
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == ValueProperty || change.Property == MaximumProperty)
{
InvalidateVisual();
}
}
public override void Render(DrawingContext context)
{
var size = Bounds.Size;
var starWidth = size.Width / Maximum;
var starHeight = size.Height;
for (int i = 0; i < Maximum; i++)
{
var starRect = new Rect(i * starWidth, 0, starWidth, starHeight);
var brush = i < Value ? Brushes.Gold : Brushes.Gray;
// Draw a simple star (or any other shape)
context.DrawRectangle(brush, null, starRect.Deflate(2));
}
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
base.OnPointerPressed(e);
var point = e.GetPosition(this);
var starWidth = Bounds.Width / Maximum;
var newValue = (int)(point.X / starWidth) + 1;
Value = newValue;
}
}
Creating a Templated Control
For more complex controls, you can create a templated control with a customizable appearance:
public class RatingControl : TemplatedControl
{
public static readonly StyledProperty<int> ValueProperty =
AvaloniaProperty.Register<RatingControl, int>(nameof(Value), defaultValue: 0);
public static readonly StyledProperty<int> MaximumProperty =
AvaloniaProperty.Register<RatingControl, int>(nameof(Maximum), defaultValue: 5);
public int Value
{
get => GetValue(ValueProperty);
set => SetValue(ValueProperty, Math.Clamp(value, 0, Maximum));
}
public int Maximum
{
get => GetValue(MaximumProperty);
set => SetValue(MaximumProperty, Math.Max(1, value));
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
// Find elements in the template
var starsPanel = e.NameScope.Find<Panel>("PART_StarsPanel");
if (starsPanel != null)
{
starsPanel.Children.Clear();
for (int i = 0; i < Maximum; i++)
{
var star = new Button
{
Content = "★",
Tag = i + 1,
Classes = { i < Value ? "selected" : "" }
};
star.Click += (sender, args) =>
{
if (sender is Button button && button.Tag is int newValue)
{
Value = newValue;
}
};
starsPanel.Children.Add(star);
}
}
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == ValueProperty)
{
var starsPanel = this.FindControl<Panel>("PART_StarsPanel");
if (starsPanel != null)
{
for (int i = 0; i < starsPanel.Children.Count; i++)
{
if (starsPanel.Children[i] is Button star)
{
if (i < Value)
star.Classes.Add("selected");
else
star.Classes.Remove("selected");
}
}
}
}
else if (change.Property == MaximumProperty)
{
// Re-apply the template to update the number of stars
InvalidateTemplate();
}
}
}
Define the control template in a style:
<Style Selector="RatingControl">
<Setter Property="Template">
<ControlTemplate>
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}">
<StackPanel Name="PART_StarsPanel" Orientation="Horizontal" />
</Border>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="RatingControl Button">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Foreground" Value="Gray" />
<Setter Property="FontSize" Value="24" />
<Setter Property="Padding" Value="2" />
</Style>
<Style Selector="RatingControl Button.selected">
<Setter Property="Foreground" Value="Gold" />
</Style>
Visual States
Visual states allow you to define different appearances for controls based on their state.
Defining Visual States
Visual states are defined using the VisualState
and VisualStateManager
classes:
<Style Selector="Button">
<Setter Property="Template">
<ControlTemplate>
<Border x:Name="border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<ContentPresenter x:Name="contentPresenter"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Padding="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
</Border>
<ControlTemplate.Transitions>
<Transitions>
<BrushTransition Property="Border.Background" Duration="0:0:0.2" />
</Transitions>
</ControlTemplate.Transitions>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="Button:pointerover /template/ Border#border">
<Setter Property="Background" Value="{StaticResource PrimaryHoverBrush}" />
</Style>
<Style Selector="Button:pressed /template/ Border#border">
<Setter Property="Background" Value="{StaticResource PrimaryPressedBrush}" />
</Style>
<Style Selector="Button:disabled /template/ Border#border">
<Setter Property="Opacity" Value="0.5" />
</Style>
Transitions
Transitions define how properties animate when they change:
<ControlTemplate.Transitions>
<Transitions>
<DoubleTransition Property="Opacity" Duration="0:0:0.2" />
<BrushTransition Property="Background" Duration="0:0:0.2" />
<ThicknessTransition Property="Margin" Duration="0:0:0.2" />
</Transitions>
</ControlTemplate.Transitions>
Custom Visual States
You can define custom visual states for your controls:
public class CustomControl : TemplatedControl
{
public static readonly StyledProperty<bool> IsExpandedProperty =
AvaloniaProperty.Register<CustomControl, bool>(nameof(IsExpanded));
public bool IsExpanded
{
get => GetValue(IsExpandedProperty);
set => SetValue(IsExpandedProperty, value);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == IsExpandedProperty)
{
PseudoClasses.Set(":expanded", IsExpanded);
}
}
}
Then you can target the custom state in styles:
<Style Selector="CustomControl:expanded">
<Setter Property="Height" Value="200" />
</Style>
Example: Creating a Custom Theme
Let's put everything together to create a custom theme for an application:
<!-- Themes/CustomTheme.axaml -->
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- Include a base theme -->
<FluentTheme />
<!-- Define colors -->
<Style.Resources>
<Color x:Key="PrimaryColor">#3F51B5</Color>
<Color x:Key="PrimaryLightColor">#7986CB</Color>
<Color x:Key="PrimaryDarkColor">#303F9F</Color>
<Color x:Key="AccentColor">#FF4081</Color>
<Color x:Key="AccentLightColor">#FF80AB</Color>
<Color x:Key="AccentDarkColor">#F50057</Color>
<Color x:Key="BackgroundColor">#FAFAFA</Color>
<Color x:Key="ForegroundColor">#212121</Color>
<Color x:Key="BorderColor">#BDBDBD</Color>
<SolidColorBrush x:Key="PrimaryBrush" Color="{StaticResource PrimaryColor}" />
<SolidColorBrush x:Key="PrimaryLightBrush" Color="{StaticResource PrimaryLightColor}" />
<SolidColorBrush x:Key="PrimaryDarkBrush" Color="{StaticResource PrimaryDarkColor}" />
<SolidColorBrush x:Key="AccentBrush" Color="{StaticResource AccentColor}" />
<SolidColorBrush x:Key="AccentLightBrush" Color="{StaticResource AccentLightColor}" />
<SolidColorBrush x:Key="AccentDarkBrush" Color="{StaticResource AccentDarkColor}" />
<SolidColorBrush x:Key="BackgroundBrush" Color="{StaticResource BackgroundColor}" />
<SolidColorBrush x:Key="ForegroundBrush" Color="{StaticResource ForegroundColor}" />
<SolidColorBrush x:Key="BorderBrush" Color="{StaticResource BorderColor}" />
</Style.Resources>
<!-- Window styles -->
<Style Selector="Window">
<Setter Property="Background" Value="{StaticResource BackgroundBrush}" />
<Setter Property="Foreground" Value="{StaticResource ForegroundBrush}" />
</Style>
<!-- Button styles -->
<Style Selector="Button">
<Setter Property="Background" Value="{StaticResource PrimaryBrush}" />
<Setter Property="Foreground" Value="White" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="15,8" />
<Setter Property="CornerRadius" Value="4" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
<Style Selector="Button:pointerover">
<Setter Property="Background" Value="{StaticResource PrimaryLightBrush}" />
</Style>
<Style Selector="Button:pressed">
<Setter Property="Background" Value="{StaticResource PrimaryDarkBrush}" />
</Style>
<Style Selector="Button:disabled">
<Setter Property="Opacity" Value="0.6" />
</Style>
<Style Selector="Button.accent">
<Setter Property="Background" Value="{StaticResource AccentBrush}" />
</Style>
<Style Selector="Button.accent:pointerover">
<Setter Property="Background" Value="{StaticResource AccentLightBrush}" />
</Style>
<Style Selector="Button.accent:pressed">
<Setter Property="Background" Value="{StaticResource AccentDarkBrush}" />
</Style>
<!-- TextBox styles -->
<Style Selector="TextBox">
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}" />
<Setter Property="Background" Value="White" />
<Setter Property="CornerRadius" Value="4" />
<Setter Property="Padding" Value="8,6" />
</Style>
<Style Selector="TextBox:focus">
<Setter Property="BorderBrush" Value="{StaticResource PrimaryBrush}" />
</Style>
<!-- CheckBox styles -->
<Style Selector="CheckBox">
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}" />
</Style>
<Style Selector="CheckBox:checked /template/ Border#PART_Border">
<Setter Property="Background" Value="{StaticResource PrimaryBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource PrimaryBrush}" />
</Style>
<!-- ComboBox styles -->
<Style Selector="ComboBox">
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}" />
<Setter Property="Background" Value="White" />
<Setter Property="CornerRadius" Value="4" />
<Setter Property="Padding" Value="8,6" />
</Style>
<Style Selector="ComboBox:focus">
<Setter Property="BorderBrush" Value="{StaticResource PrimaryBrush}" />
</Style>
<!-- ListBox styles -->
<Style Selector="ListBox">
<Setter Property="Background" Value="White" />
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="4" />
</Style>
<Style Selector="ListBoxItem:selected">
<Setter Property="Background" Value="{StaticResource PrimaryLightBrush}" />
<Setter Property="Foreground" Value="White" />
</Style>
<!-- TabControl styles -->
<Style Selector="TabControl">
<Setter Property="Background" Value="White" />
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="4" />
</Style>
<Style Selector="TabItem">
<Setter Property="Padding" Value="15,8" />
<Setter Property="Foreground" Value="{StaticResource ForegroundBrush}" />
</Style>
<Style Selector="TabItem:selected">
<Setter Property="Background" Value="{StaticResource PrimaryBrush}" />
<Setter Property="Foreground" Value="White" />
</Style>
<!-- More control styles... -->
</Styles>
Apply the custom theme in your application:
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="MyApp.App">
<Application.Styles>
<StyleInclude Source="/Themes/CustomTheme.axaml" />
</Application.Styles>
</Application>
This custom theme:
- Defines a color palette with primary, accent, and neutral colors
- Applies consistent styling to common controls
- Includes hover, pressed, and disabled states
- Provides an accent variant for buttons
- Maintains a cohesive look and feel across the application
In the next section, we'll explore handling events and commands in Avalonia UI.