Guidelines for modern Avalonia UI layout using Zafiro.Avalonia, emphasizing shared styles, generic components, and avoiding XAML redundancy.
Master modern, clean, and maintainable Avalonia UI layouts. Focus on semantic containers, shared styles, and minimal XAML.
Every view, page, or component you create MUST use responsive panels. This is not optional. Zafiro.Avalonia targets Desktop, Mobile, and Browser — a layout that only works at one size is a bug.
Before writing any AXAML layout, ask yourself:
FlexPanel (not StackPanel).BootstrapGridPanel with per-breakpoint spans (not Grid/UniformGrid).SemanticPanel or BlueprintPanel.Read responsive.md before any layout work. It contains the complete reference.
Read ONLY files relevant to the layout challenge!
| File | Description | When to Read |
|---|---|---|
responsive.md | Responsive layout: FlexPanel, BootstrapGridPanel, nesting, panel selection, responsive typography | Any layout that must adapt to different screen sizes (ALWAYS read this first) |
themes.md | Theme organization and shared styles | Setting up or refining app themes |
containers.md | Semantic containers (HeaderedContainer, EdgePanel, Card) | Structuring views and layouts |
icons.md | Icon usage with IconExtension and IconOptions | Adding and customizing icons |
behaviors.md | Xaml.Interaction.Behaviors, avoiding Converters, DragDeltaBehavior | Implementing complex interactions, drag-to-move |
components.md | Generic components, FlowEditor, PropertyGrid | Creating reusable UI elements, node editors, inspector panels |
scripts/avalonia_style_probe.py | Style/resource scanner with computed-style estimate | Before applying local visual attributes |
For a real-world example, refer to the Angor project:
/mnt/fast/Repos/angor/src/Angor/Avalonia/Angor.Avalonia.sln
FlexPanel/BootstrapGridPanel instead of fixed Grid/StackPanel? (See responsive.md)Window.Compact class toggle for mobile font sizes? Never ContainerQuery on MainWindow for global typography? (See responsive.md Trick 5)Col (base) first, then override with ColMd, ColLg, etc.?Grow/Shrink/MarginLeftAuto instead of fixed StackPanel?HeaderedContainer instead of Border with manual header)HeaderedContainer for title+body cards? If a block is "header + content", prefer HeaderedContainer with Header, Content, HeaderClasses, ContentClasses.axaml files.scripts/avalonia_style_probe.py before adding local attributes.EdgePanel or generic components.{Icon fa-name} and IconOptions for styling.Interaction.Behaviors for UI-logic.DynamicResource backed by ThemeDictionaries, not hardcoded literals.DON'T:
BootstrapGridPanel or FlexPanel instead.UniformGrid Columns="3" for cards. Use BootstrapGridPanel with per-breakpoint spans.StackPanel Orientation="Horizontal" for toolbars. Use FlexPanel with Wrap="Wrap".Grid and StackPanel.IValueConverter for simple logic that belongs in the ViewModel.Border + StackPanel + TextBlock(header) + TextBlock(content) when HeaderedContainer fits.<EnhancedButton Command="{Binding Invest}" DockPanel.Dock="Right" Classes="Outline">
<StackPanel Orientation="Horizontal" Spacing="10">
<TextBlock Text="📈" />
TextBlock Text="Invest Now" />
</StackPanel>
</EnhancedButton>
Instead, go with
<EnhancedButton Icon="{Icon fa-chart-line}" Content="Invest Now" />
DO:
DynamicResource for colors and brushes.Zafiro.Avalonia specific panels like EdgePanel for common UI patterns.Use this decision rule during layout work:
HeaderedContainer.EdgePanel inside HeaderedContainer (or as header content).Border for purely decorative wrappers with no semantic header/content meaning.Preferred pattern (card-like block):
<HeaderedContainer Classes="Card White Radius-M"
Header="Desktop App"
Content="Coming soon"
HeaderClasses="Size-L Weight-Bold Text-Strong"
ContentClasses="Size-S Text-Muted" />
Avoid when equivalent semantic container exists:
<Border Classes="ColorPanel White Radius-M">
<StackPanel Classes="Gap-XS">
<TextBlock Classes="Size-L Weight-Bold Text-Strong">Desktop App</TextBlock>
<TextBlock Classes="Size-S Text-Muted">Coming soon</TextBlock>
</StackPanel>
</Border>
Compression is good only when intent remains obvious on first read.
Header, Content, HeaderClasses, ContentClasses) over "magic" local aliases when scope is small.For stateful visuals (hover, selected, focus, pressed):
ThemeDictionaries (Light and Dark).{DynamicResource ...}.Pattern:
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<SolidColorBrush x:Key="Highlight.Background">#F9FBFB</SolidColorBrush>
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<SolidColorBrush x:Key="Highlight.Background">#1F2937</SolidColorBrush>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Style Selector=":is(TemplatedControl):pointerover.Highlight">
<Setter Property="Background" Value="{DynamicResource Highlight.Background}" />
</Style>
Before touching any local visual property (Background, Foreground, FontSize, Padding, etc.), the agent must inspect the project's style/resource graph and estimate the final computed properties for the target control.
Use:
python3 /home/jmn/skills/avalonia-layout-zafiro/scripts/avalonia_style_probe.py \
--project-root <repo-root> \
--control <ControlType> \
--classes <Class1,Class2> \
--show-tree
Resolution mode:
App.axaml/App.xaml, fallback Styles.axaml/Styles.xaml) and traverses transitive includes from there.--root <file> to force one or more root files explicitly.--scan-all only for exploratory scans (less faithful to runtime).Rules:
Principle: Never invent class names (e.g., H1, Card, Caption). Instead, scan the project resources to find the definitions.
Search for the theme configuration. Common locations:
App.axaml: Often contains <StyleInclude> for the main theme.Themes/ or Styles/ directories: Look for .axaml files.Styles.axaml: In library projects.Open the relevant .axaml files (e.g., Typography.axaml, Buttons.axaml, Containers.axaml) and search for Style Selector.
Pattern to look for:
<Style Selector="TextBlock.Title"> ... </Style>
<Style Selector="Border.Panel"> ... </Style>
<Style Selector="HeaderedContainer.WizardSection"> ... </Style>
The part after the dot (e.g., Title, Panel) is the Class name.
Run scripts/avalonia_style_probe.py with the target control and classes to estimate:
StaticResource / DynamicResource resolution when available)Once you confirm the class exists, apply it using the Classes property.
<!-- ✅ CORRECT: Verified class 'Title' exists in Typography.axaml -->
<TextBlock Classes="Title" Text="My Header" />
<!-- ❌ INCORRECT: Guessing a class name -->
<TextBlock Classes="H1" Text="My Header" />
If you can't find a class, look for ControlTheme or StaticResource keys in ResourceDictionary.
<!-- Finding the key -->
<ControlTheme x:Key="TransparentButton" ... />
<!-- Applying it -->
<EnhancedButton Theme="{StaticResource TransparentButton}" ... />
When a header is just a string (e.g., Header="Project Statistics"), you cannot "apply a style to another style".
Wrap the header with a HeaderTemplate and use atomic classes on a TextBlock.
<Style Selector="HeaderedContainer.Big">
<Setter Property="HeaderTemplate">
<DataTemplate>
<TextBlock Classes="Size-XL Weight-Bold" Text="{Binding}" />
</DataTemplate>
</Setter>
</Style>
Notes:
Title) if they match your intent.While every project is different, Zafiro-based projects often use these semantic patterns. Scan files to confirm:
Title, Subtitle, Header, Caption, or atomic sizes (Size-S, Size-L) and weights (Weight-Bold).Panel (often a Border with shadow/bg) or Section styles for HeaderedContainer.Primary, Secondary) or visual styles (Outline, Ghost/Transparent).