---
name: frontend-ui-engineering-avalonia
description: Builds production-quality Avalonia UIs — MVVM with CommunityToolkit.Mvvm, compiled bindings (`x:DataType`, `x:CompileBindings`), `DataTemplate`s, theming via FluentTheme/SimpleTheme with `ThemeVariant`, keyboard navigation, `AutomationProperties` for accessibility, adaptive layouts across desktop/mobile/browser targets. Use when building or modifying Avalonia 11/12 user interfaces, implementing views and view-models, or fixing visual or UX issues in an Avalonia app.
version: 0.5.0
source: rewritten from vendor/agent-skills/skills/frontend-ui-engineering/SKILL.md@44dac80
---

<!-- Adapted from addyosmani/agent-skills (MIT © 2025 Addy Osmani). This is a STRUCTURAL REWRITE — upstream targets React/TypeScript/Tailwind UI. This skill retargets the same goals (production-quality, accessible, performant UIs without the generic "AI aesthetic") to Avalonia 11/12 with CommunityToolkit.Mvvm. The cross-cutting principles (component focus, design-system adherence, accessibility, loading/error/empty states, avoid AI aesthetic) survive; every concrete mechanism is Avalonia-specific. See the "Source & Modifications" footer for the full delta. For Blazor / ASP.NET Core MVC / MAUI the principles still apply; framework-specific siblings can be added in a later wave. -->

# Frontend UI Engineering — Avalonia

## Overview

Build production-quality Avalonia UIs that are accessible, performant, and visually polished. The goal is UI that looks like it was built by a design-aware .NET engineer at a top company — not like it was generated by an AI. That means real design-system adherence, proper keyboard navigation and screen-reader support, compiled XAML bindings for performance and type safety, thoughtful theming that respects light/dark/system variants, and no generic "AI aesthetic."

## When to Use

- Building new Avalonia views or windows
- Modifying existing user-facing interfaces
- Implementing adaptive layouts for desktop + mobile + browser targets
- Adding interactivity or state management in view-models
- Fixing visual or UX issues (binding failures, theming bugs, focus traps)
- Migrating between Avalonia versions (11.x ↔ 12.x)

**When NOT to use:** backend-only changes in `MyApp.Core` / `MyApp.Infrastructure`, CLI tools, or code that doesn't render in an Avalonia view.

**Related:**
- [`context-engineering`](../context-engineering/SKILL.md) for setting up CLAUDE.md + `.editorconfig` for an Avalonia project
- [`integration-testing-dotnet`](../integration-testing-dotnet/SKILL.md) for testing views with `Avalonia.Headless.XUnit`
- [`performance-optimization-dotnet`](../performance-optimization-dotnet/SKILL.md) for virtualized lists, binding overhead, and startup-time tuning
- The companion **avalonia-dev** plugin in this marketplace (`/avalonia-review`) for structural reviews of existing Avalonia solutions — design token extraction, theming, project layout, phased migration

## Version Awareness

This skill supports Avalonia **11.x** and **12.x**. Where a technique differs between versions, the pattern is flagged.

Key differences to keep front-of-mind (see Avalonia's official migration guide for the full list):
- **v12** enables compiled bindings (`x:CompileBindings`) by default on `UserControl` / `Window`; **v11** defaults to off — set it explicitly so your bindings are typed and validated at build time.
- **v12** replaces `ReactiveObject`-first defaults with first-class support for `CommunityToolkit.Mvvm`'s source-generator patterns.
- **v12**'s styling API tightens selector precedence; double-check custom styles after migrating.
- **v12** page-based navigation APIs (`Navigation.PushAsync`) are first-party; v11 patterns via `ReactiveUI.Routing` still work but are no longer the canonical choice.

When you detect the project's version (from the `Avalonia` NuGet reference in `Directory.Packages.props` / the UI `.csproj`), state it explicitly and apply version-appropriate patterns.

**Side-by-side: the compiled-bindings opt-in**

```xml
<!-- Avalonia 11 — compiled bindings are OFF by default. Opt in explicitly. -->
<UserControl xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:vm="using:MyApp.ViewModels"
             x:DataType="vm:TaskListViewModel"
             x:CompileBindings="True">
    <!--
        Without x:CompileBindings="True", v11 resolves every {Binding Title}
        at runtime using reflection. Typos become silent failures — the
        view renders blank and the problem surfaces only in the debug output.
    -->
</UserControl>
```

```xml
<!-- Avalonia 12 — compiled bindings are ON by default. Just declare the type. -->
<UserControl xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:vm="using:MyApp.ViewModels"
             x:DataType="vm:TaskListViewModel">
    <!--
        x:CompileBindings is implicit. Forgetting x:DataType here means the
        compiled bindings have nothing to check against and individual bindings
        fall back to runtime resolution — so always declare the type anyway.
    -->
</UserControl>
```

The visible XAML diff is one attribute on the root element. The cost of getting it wrong is version-specific: **v11** without `x:CompileBindings="True"` silently degrades to runtime reflection; **either version** without `x:DataType` loses the build-time type check. Default posture: declare both on every `UserControl` / `Window` regardless of Avalonia version — it's correct in v11 and redundant-but-harmless in v12.

## Component Architecture

### File Structure

Colocate everything related to a view. Avalonia projects typically separate `Views/` and `ViewModels/` at the top level; for larger features, colocate by feature:

```
src/MyApp/
  Views/
    Tasks/
      TaskListView.axaml           # View markup
      TaskListView.axaml.cs         # Code-behind (minimal — one per view)
      TaskItemTemplate.axaml        # Small reusable DataTemplate if shared
  ViewModels/
    Tasks/
      TaskListViewModel.cs          # [ObservableObject] with [ObservableProperty] / [RelayCommand]
      TaskItemViewModel.cs
  Styles/
    Tasks/
      TaskList.axaml                # Feature-specific styles
  Resources/
    Tokens/
      Colors.axaml                  # Design-token resource dictionary
      Spacing.axaml
```

Larger apps may prefer feature-folders:

```
src/MyApp/Features/Tasks/
  TaskListView.axaml
  TaskListView.axaml.cs
  TaskListViewModel.cs
  TaskItemViewModel.cs
  Styles.axaml
```

Either layout is fine — pick one per project and stick to it. See the **avalonia-dev** plugin's `project-structure` reference for worked examples.

### Keep Views and View-Models Focused

Each view does one thing. Each view-model exposes only what its view needs:

```csharp
// Good: Single-responsibility view-model
public sealed partial class TaskItemViewModel : ObservableObject
{
    public TaskItemViewModel(TaskDto task, ITaskService service)
    {
        _service = service;
        Id       = task.Id;
        Title    = task.Title;
        IsDone   = task.Status == TaskStatus.Completed;
    }

    private readonly ITaskService _service;

    public TaskId Id { get; }

    [ObservableProperty] private string _title = string.Empty;
    [ObservableProperty] private bool _isDone;

    [RelayCommand]
    private async Task ToggleAsync(CancellationToken cancellationToken)
    {
        IsDone = !IsDone;
        await _service.SetStatusAsync(Id, IsDone ? TaskStatus.Completed : TaskStatus.Pending, cancellationToken);
    }
}
```

```xml
<!-- Corresponding view snippet (compiled bindings on) -->
<Border x:DataType="vm:TaskItemViewModel"
        x:CompileBindings="True"
        Padding="12"
        Classes="task-item">
  <StackPanel Orientation="Horizontal" Spacing="12">
    <CheckBox IsChecked="{Binding IsDone}"
              Command="{Binding ToggleCommand}"
              AutomationProperties.Name="{Binding Title, StringFormat='Toggle task: {0}'}" />
    <TextBlock Text="{Binding Title}"
               Classes.done="{Binding IsDone}" />
  </StackPanel>
</Border>
```

### Prefer Composition Over Configuration

Build complex views by composing small controls, not by adding more properties:

```xml
<!-- Good: Composable -->
<Card>
  <Card.Header>
    <TextBlock Text="Tasks" Classes="h2" />
  </Card.Header>
  <ListBox ItemsSource="{Binding Tasks}"
           ItemTemplate="{StaticResource TaskItemTemplate}" />
</Card>

<!-- Avoid: Over-configured -->
<Card Title="Tasks"
      HeaderVariant="Large"
      BodyPadding="Medium"
      Items="{Binding Tasks}"
      ItemTemplateKey="TaskItemTemplate" />
```

### Separate Loading / Error / Empty from Presentation

Every list or async-loaded surface needs four states: loading, error, empty, populated. Surface them explicitly:

```xml
<Panel>
  <!-- Loading -->
  <ProgressBar IsIndeterminate="True"
               IsVisible="{Binding IsLoading}"
               VerticalAlignment="Center" />

  <!-- Error -->
  <StackPanel IsVisible="{Binding HasError}" Spacing="8" Margin="24">
    <TextBlock Text="Couldn't load tasks." Classes="h3" />
    <TextBlock Text="{Binding ErrorMessage}" TextWrapping="Wrap" />
    <Button Content="Retry" Command="{Binding LoadCommand}" />
  </StackPanel>

  <!-- Empty -->
  <StackPanel IsVisible="{Binding IsEmpty}" Spacing="8" Margin="24" HorizontalAlignment="Center">
    <PathIcon Data="{StaticResource EmptyTasksIcon}" Width="48" Height="48" />
    <TextBlock Text="No tasks yet" Classes="h3" />
    <TextBlock Text="Create one to get started." Classes="body-muted" />
    <Button Content="Create task" Command="{Binding NewTaskCommand}" Classes="accent" />
  </StackPanel>

  <!-- Populated -->
  <ListBox IsVisible="{Binding HasTasks}"
           ItemsSource="{Binding Tasks}"
           ItemTemplate="{StaticResource TaskItemTemplate}" />
</Panel>
```

View-model exposes the four states as discrete flags so the binding expressions stay trivially readable.

## Binding Discipline

### Compiled Bindings Are Not Optional

Turn on `x:CompileBindings="True"` (default in v12, opt-in on v11) and always specify `x:DataType`. Runtime-resolved bindings are the #1 source of "why is my view blank?" bugs — the failure is logged to the debug output and nowhere else.

```xml
<!-- Declare at the root of every view -->
<UserControl xmlns:vm="using:MyApp.ViewModels.Tasks"
             x:DataType="vm:TaskListViewModel"
             x:CompileBindings="True">
  <!-- All bindings below are type-checked at build time -->
</UserControl>
```

For `DataTemplate`s that bind to a different type, scope the data type explicitly:

```xml
<DataTemplate x:DataType="vm:TaskItemViewModel">
  <Grid ColumnDefinitions="Auto,*,Auto">
    <CheckBox IsChecked="{Binding IsDone}" />
    <TextBlock Grid.Column="1" Text="{Binding Title}" />
    <Button Grid.Column="2" Content="Delete" Command="{Binding DeleteCommand}" />
  </Grid>
</DataTemplate>
```

### Binding Diagnostics

When a binding is "wrong but silent," enable binding diagnostics in development:

```csharp
// App.axaml.cs  (or wherever you call AppBuilder)
Logger.Sink = new TraceLogSink(LogEventLevel.Warning, new[] { LogArea.Binding, LogArea.Property });
```

Any failed or unresolved binding writes to the debug console. Fix all of them before shipping.

## State Management

Pick the simplest approach that works. Avalonia view-model state is typically held in the view-model itself; for cross-view state prefer DI-scoped services, not ambient statics:

```
ObservableObject + ObservableProperty          → Single-view state (the common case)
Shared service injected via DI                  → State shared between view-models (registered as Singleton or Scoped)
Messenger (CommunityToolkit.Mvvm)                → Loosely-coupled notifications between view-models (subscribe/publish)
IOptions<T> / IOptionsMonitor<T>                 → Configuration-style state bound from appsettings.json
ReactiveUI (if the project already uses it)      → Reactive chains with observables; v11 legacy, still supported in v12
```

**Avoid `INotifyPropertyChanged` boilerplate** by standardising on `CommunityToolkit.Mvvm` source generators (`[ObservableProperty]`, `[RelayCommand]`, `[NotifyCanExecuteChangedFor]`). They produce the plumbing at compile time — no reflection, no runtime cost.

## Design-System Adherence

### Tokens, Not Raw Values

Bind to resources, not hard-coded values. A design token layer lives in `Resources/Tokens/*.axaml` and is merged into the application's resource dictionary:

```xml
<!-- Resources/Tokens/Colors.axaml -->
<ResourceDictionary xmlns="https://github.com/avaloniaui">
  <ResourceDictionary.ThemeDictionaries>
    <ResourceDictionary x:Key="Light">
      <Color x:Key="SurfaceColor">#FFFFFF</Color>
      <Color x:Key="TextPrimaryColor">#0F172A</Color>
      <Color x:Key="AccentColor">#2563EB</Color>
    </ResourceDictionary>
    <ResourceDictionary x:Key="Dark">
      <Color x:Key="SurfaceColor">#0B1220</Color>
      <Color x:Key="TextPrimaryColor">#F1F5F9</Color>
      <Color x:Key="AccentColor">#60A5FA</Color>
    </ResourceDictionary>
  </ResourceDictionary.ThemeDictionaries>

  <SolidColorBrush x:Key="SurfaceBrush" Color="{DynamicResource SurfaceColor}" />
  <SolidColorBrush x:Key="TextPrimaryBrush" Color="{DynamicResource TextPrimaryColor}" />
  <SolidColorBrush x:Key="AccentBrush" Color="{DynamicResource AccentColor}" />
</ResourceDictionary>
```

Use `DynamicResource` for anything theme-sensitive (colors, brushes) and `StaticResource` for things that don't change at runtime (spacing tokens, icon geometry). See the **avalonia-dev** plugin's `design-tokens` reference for a full token taxonomy (colors, typography, spacing, elevation).

### Spacing

Use a spacing scale. 4 px / 8 px increments are common — `SpacingXs=4`, `SpacingSm=8`, `SpacingMd=12`, `SpacingLg=16`, `SpacingXl=24`. Don't invent `Padding="13"`.

### Typography

Respect the type hierarchy. One `h1` per window / page; never use heading classes for non-heading content:

```xml
<TextBlock Text="{Binding Title}" Classes="h1" />
<TextBlock Text="{Binding Subtitle}" Classes="h2" />
<TextBlock Text="{Binding Body}" Classes="body" TextWrapping="Wrap" />
<TextBlock Text="{Binding Caption}" Classes="caption text-muted" />
```

Define the classes once in `Styles/Typography.axaml`. Don't set per-element `FontSize="14"` — that's how type hierarchies decay.

### Avoid the AI Aesthetic

AI-generated Avalonia UIs have recognizable patterns. Avoid all of them:

| AI Default | Why It's a Problem | Production Quality |
|---|---|---|
| Purple/indigo everywhere | Models default to visually "safe" palettes so every app looks identical | Use the project's actual color palette via resource tokens |
| Maximum corner radius on everything | `CornerRadius="16"` on every `Border` flattens hierarchy and signals "friendly"-but-generic | Corner radius from a token scale (e.g. Xs/Sm/Md) that matches control size |
| Over-nested `StackPanel`/`Grid` | Layouts that are 8 levels deep for a single row | `Grid` with `ColumnDefinitions` / `DockPanel` / `StackPanel` picked per role, not stacked by habit |
| Lorem-ipsum-style sample data | Placeholder content hides layout problems real content reveals (long titles wrap, numbers push columns) | Realistic fixtures in the design-time `DataContext` |
| Oversized padding everywhere | `Padding="24"` on every `Border` destroys hierarchy and wastes space on small windows | Padding from the spacing token scale, stepped by container role |
| Uniform stock card grids | Grid-of-cards layout regardless of content priority | Purpose-driven layouts — `DockPanel` for master/detail, `Grid` for true tabular data, `ListBox` with a real item template |
| Shadow-heavy design | Layered shadows and "glassmorphism" compete with content and slow rendering on low-end devices | Subtle or no shadows unless the design system specifies them |
| `Fluent` theme with no customization | Works but looks like every other Avalonia starter | Customize the accent color / control templates / corner radii to match your product |

### Design-Time Data

Populate a design-time `DataContext` so XAML previewers show real-looking content — long titles, mixed states, populated lists. Bad design-time data is how the AI aesthetic slips in: if your previewer only ever shows one short title, you won't notice when production overflows.

```xml
<Design.DataContext>
  <vm:TaskListViewModel>
    <!-- Design-time sample, not used at runtime -->
  </vm:TaskListViewModel>
</Design.DataContext>
```

## Theming

Use `FluentTheme` (or `SimpleTheme`) as the foundation and layer your overrides on top. Don't build a theme from scratch unless the product requires it.

```xml
<!-- App.axaml -->
<Application.Styles>
  <FluentTheme />
  <StyleInclude Source="avares://MyApp/Styles/Typography.axaml" />
  <StyleInclude Source="avares://MyApp/Styles/Buttons.axaml" />
</Application.Styles>

<Application.Resources>
  <ResourceInclude Source="avares://MyApp/Resources/Tokens/Colors.axaml" />
  <ResourceInclude Source="avares://MyApp/Resources/Tokens/Spacing.axaml" />
</Application.Resources>
```

Respect `ThemeVariant.Default` — if the user's OS is in dark mode and you've defined Light + Dark dictionaries, Avalonia swaps them automatically.

```csharp
// To toggle explicitly (settings-driven override):
Application.Current!.RequestedThemeVariant = ThemeVariant.Dark;
```

## Accessibility

Every Avalonia control must meet these expectations. Windows Narrator, macOS VoiceOver, and Linux Orca all read `AutomationProperties` — set them.

### Keyboard Navigation

```xml
<!-- Every interactive control must be reachable via Tab -->
<Button Content="Save" Command="{Binding SaveCommand}" />                            <!-- ✓ Button is natively focusable -->
<Border PointerPressed="OnBorderClicked"
        Focusable="True"                                                              <!-- ✓ Must be set explicitly on non-control panels -->
        KeyDown="OnBorderKeyDown">                                                    <!-- ✓ Handle Enter/Space manually if you chose Border -->
  <TextBlock Text="Click me" />
</Border>
<!-- Avoid: using Border for interactions when a Button works. Prefer the semantic control. -->
```

Set `TabIndex` only when the default focus order is wrong, and set explicit focus when a view is activated:

```csharp
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
    base.OnAttachedToVisualTree(e);
    TitleTextBox.Focus();
}
```

### `AutomationProperties`

```xml
<!-- Name interactive controls that lack visible text -->
<Button Command="{Binding CloseCommand}"
        AutomationProperties.Name="Close dialog">
  <PathIcon Data="{StaticResource CloseIcon}" />
</Button>

<!-- Label form inputs via AutomationProperties.LabeledBy -->
<StackPanel Spacing="4">
  <TextBlock x:Name="EmailLabel" Text="Email" />
  <TextBox Text="{Binding Email}"
           AutomationProperties.LabeledBy="{Binding #EmailLabel}" />
</StackPanel>

<!-- Describe live regions for dynamic content -->
<TextBlock Text="{Binding StatusMessage}"
           AutomationProperties.LiveSetting="Assertive"
           AutomationProperties.Name="Status" />
```

### Focus Management in Dialogs

```csharp
public partial class ConfirmDialog : Window
{
    public ConfirmDialog()
    {
        InitializeComponent();
        Opened += (_, _) => ConfirmButton.Focus();
    }
}
```

For modal dialogs, also trap focus — Avalonia's `ShowDialog` takes care of it for you when `DialogResult` is awaited.

### Color Contrast

- Normal text vs surface: at least **4.5:1** (WCAG 2.1 AA)
- Large text (18pt+): at least **3:1**
- Never use color as the sole signal. Pair red with an icon, an error message, or a pattern; pair "status" colors with text.

### Verify with Real Tools

- **Windows**: Accessibility Insights for Windows — inspects the automation tree, flags missing `Name`/`Role`, checks contrast
- **macOS**: VoiceOver rotor (`Ctrl+Option+U`) against the running app
- **Linux**: Accerciser + Orca for spot-checks

## Adaptive Layouts

Avalonia runs on Windows, macOS, Linux, iOS, Android, and the browser. A single XAML file can serve all of them if you design adaptively.

### Use Responsive Units

```xml
<Grid ColumnDefinitions="Auto,*,Auto" RowDefinitions="Auto,*">
  <!-- Let content dictate sidebar width; main content flexes -->
</Grid>
```

### Class-Based Layout Switching

```xml
<UserControl Classes.compact="{Binding IsCompactLayout}">
  <Panel>
    <!-- Desktop layout: master + detail -->
    <Grid IsVisible="{Binding !IsCompactLayout}" ColumnDefinitions="280,*">
      <ListBox Grid.Column="0" ItemsSource="{Binding Tasks}" />
      <ContentControl Grid.Column="1" Content="{Binding SelectedTask}" />
    </Grid>

    <!-- Compact layout: single-pane navigation -->
    <ContentControl IsVisible="{Binding IsCompactLayout}"
                    Content="{Binding CurrentPane}" />
  </Panel>
</UserControl>
```

Determine `IsCompactLayout` via `this.GetObservable(Window.BoundsProperty)` or a `Window`-level `LayoutUpdated` handler.

### Mobile Targets

For the mobile head of the app, check `OperatingSystem.IsAndroid()` / `OperatingSystem.IsIOS()` in the composition root, and avoid relying on window sizing cues that work on desktop only.

## Async UI Patterns

### Stay Off the UI Thread for Work

```csharp
[RelayCommand]
private async Task LoadAsync(CancellationToken cancellationToken)
{
    IsLoading = true;
    try
    {
        var tasks = await _service.ListAsync(cancellationToken).ConfigureAwait(true);
        // `await` resumes on the UI thread by default in an Avalonia view-model —
        // assigning back to bound properties is safe.
        Tasks = tasks;
    }
    catch (OperationCanceledException) { /* ignore */ }
    catch (Exception ex)
    {
        ErrorMessage = ex.Message;
    }
    finally
    {
        IsLoading = false;
    }
}
```

Never block the dispatcher with `.Result` or `.Wait()`. In async code in a UI project, every I/O operation accepts a `CancellationToken` wired through to the view-model's lifecycle token — tie it to view disposal so navigating away cancels in-flight work.

### Marshalling to the UI Thread

When you start background work off the UI thread, marshal back explicitly:

```csharp
await Dispatcher.UIThread.InvokeAsync(() =>
{
    Tasks = updated;
});
```

## Testing

Avalonia UI tests run in-process via `Avalonia.Headless.XUnit`. See [`integration-testing-dotnet`](../integration-testing-dotnet/SKILL.md) for the setup and representative tests. At minimum, every critical view should have a test that:

1. Creates the view in a headless `Window`
2. Pumps the dispatcher (`Dispatcher.UIThread.RunJobs()`)
3. Asserts that bindings produced the expected state
4. Triggers a command and re-asserts

## Performance

See [`performance-optimization-dotnet`](../performance-optimization-dotnet/SKILL.md) for the full treatment. The Avalonia-specific highlights:

- **Virtualize long lists** with `ListBox` + `VirtualizingStackPanel` (the default). Never swap in a plain `StackPanel` for a list of >100 items.
- **Compiled bindings** (`x:CompileBindings="True"`, `x:DataType="..."`) — faster binding and type-checked at build
- **Avoid deep visual trees** — use `DockPanel` / `Grid` rather than nested `StackPanel`s when the shape allows
- **Prefer `DynamicResource` only for genuinely theme-sensitive properties** — every `DynamicResource` subscribes to a resource-change event
- **Profile cold-start** — `dotnet-trace` with the `StartupPerformance` profile plus `Application.Current.Startup` instrumentation. An AOT-published build (`dotnet publish -r <rid> -p:PublishAot=true` on v12) can halve startup on desktop targets

## Common Rationalizations

| Rationalization | Reality |
|---|---|
| "Binding errors don't matter if the view renders" | They mean a property isn't wired up. The view is showing stale or default data, and the debug log is screaming. Turn on diagnostics, fix every one. |
| "Compiled bindings are a premature optimization" | They're a correctness tool, not an optimization. Typos in binding paths become compile errors instead of silent runtime failures. |
| "Accessibility is a nice-to-have" | It's a legal requirement in many jurisdictions and a baseline quality standard. Screen readers find bugs your eyes don't — try narrating your view once. |
| "We'll theme it later" | Retrofitting theming onto a view that has `Background="#FFFFFF"` everywhere is more work than doing it right. Use `DynamicResource` from the start for colors and brushes. |
| "This is just an internal tool, design doesn't matter" | Internal tools get used daily by your team — poor design costs real time. Spend an hour on the token scale now; save hours of friction later. |
| "The Fluent theme is enough" | Fluent is a foundation, not a product. Pick an accent color, tune the corner radii, define your type scale — otherwise your app looks like every other Avalonia starter. |

## Red Flags

- Views with more than ~300 lines of XAML (split into sub-views or `ContentControl`s)
- Hard-coded colors, spacings, or font sizes (should be tokens)
- `x:CompileBindings="False"` or missing `x:DataType` on v11 views
- `StackPanel` for a list of >100 items (use `ListBox` / `VirtualizingStackPanel`)
- Missing loading / error / empty states
- No `AutomationProperties.Name` on icon-only buttons
- `.Result` / `.Wait()` inside a `RelayCommand` body
- Binding warnings in the debug output ignored as "known"
- Color as the sole indicator of state (red/green without text or icons)
- Generic "AI look" — purple accents, `CornerRadius="16"` everywhere, oversized padding, uniform card grids

## Verification

After building or modifying an Avalonia view:

- [ ] View compiles with no XAML warnings; `x:DataType` + `x:CompileBindings="True"` declared at root
- [ ] Debug output has zero binding failures when the view is exercised
- [ ] Every interactive control is reachable via Tab, in a sensible order
- [ ] Every icon-only button has `AutomationProperties.Name`
- [ ] Loading, error, empty, and populated states all have explicit markup
- [ ] Works in both Light and Dark `ThemeVariant`
- [ ] Colors, spacing, typography use resource tokens (no raw hex, no magic pixel numbers)
- [ ] No accessibility issues flagged by Accessibility Insights / VoiceOver / Orca
- [ ] `Avalonia.Headless.XUnit` test exercises the critical binding and at least one command
- [ ] Cold-start hasn't regressed (measure before/after for significant view additions)

---

## Source & Modifications

- **Upstream**: https://github.com/addyosmani/agent-skills/blob/44dac80216da709913fb410f632a65547866346f/skills/frontend-ui-engineering/SKILL.md
- **Pinned commit**: `44dac80216da709913fb410f632a65547866346f` (synced 2026-04-19)
- **Status**: `rewritten`
- **Rationale**: upstream targets React / TypeScript / Tailwind — every concrete mechanism (JSX, hooks, Tailwind classes, React re-render patterns, the `<picture>` element for LCP) is inapplicable to Avalonia XAML. The cross-cutting principles (component focus, composition, design-system adherence, accessibility baseline, loading/error/empty states, avoid the AI aesthetic, responsive design) translate, but the body of the skill is retargeted to Avalonia 11 / 12 with `CommunityToolkit.Mvvm`, compiled XAML bindings, resource dictionaries, `AutomationProperties`, and theme-variant awareness.
- **What changed (almost everything)**:
  - New skill name (`frontend-ui-engineering-avalonia`) — sibling slots can be added for Blazor / ASP.NET Core MVC / MAUI in a later wave
  - New "Version Awareness" section calling out Avalonia 11 vs 12 differences (compiled bindings default, styling API, navigation API, CommunityToolkit.Mvvm first-class)
  - File-structure example uses `Views/`, `ViewModels/`, `Resources/Tokens/`, `Styles/` — names real Avalonia project conventions
  - Component example uses `[ObservableObject]` / `[ObservableProperty]` / `[RelayCommand]` with a corresponding XAML snippet declaring `x:DataType` + `x:CompileBindings`
  - "Prefer composition over configuration" example rewritten in XAML
  - "Separate loading / error / empty / populated" example rewritten as an Avalonia `Panel` with four `IsVisible`-gated child layouts
  - New "Binding Discipline" section — compiled bindings + binding diagnostics — has no upstream equivalent
  - State management list retargeted to `ObservableObject`, DI-scoped services, `Messenger`, `IOptions<T>`, `ReactiveUI` for v11 legacy
  - "Tokens, Not Raw Values" rewritten with `ResourceDictionary.ThemeDictionaries` for Light/Dark variants and `DynamicResource` vs `StaticResource` guidance
  - Spacing + typography + color-as-tokens kept conceptually; mechanisms swapped to XAML style classes and resource brushes
  - "Avoid the AI Aesthetic" table re-written with Avalonia-specific smells (`CornerRadius="16"`, deep `StackPanel` nesting, unchanged Fluent theme, oversized padding, uniform card grids, shadow-heavy / "glassmorphism" looks, lorem-ipsum in `Design.DataContext`)
  - New "Design-Time Data" section on `Design.DataContext` for XAML previewer fidelity
  - New "Theming" section (`FluentTheme` + overrides + `ThemeVariant`) — has no upstream analog
  - "Accessibility" rewritten for `AutomationProperties.Name`/`LabeledBy`/`LiveSetting`, Narrator/VoiceOver/Orca, Accessibility Insights for Windows
  - "Responsive Design" → "Adaptive Layouts" rewritten for class-based layout switching + `OperatingSystem.IsAndroid()`/`IsIOS()` for mobile targets
  - New "Async UI Patterns" section (`RelayCommand` body, `OperationCanceledException` handling, `Dispatcher.UIThread.InvokeAsync`)
  - Testing pointer goes to `integration-testing-dotnet` (`Avalonia.Headless.XUnit`); performance pointer goes to `performance-optimization-dotnet`
  - Common Rationalizations table fully rewritten for Avalonia concerns (binding errors, compiled bindings, Fluent theme defaults, retrofitting theming)
  - Red Flags list fully rewritten for Avalonia concerns (long XAML, hard-coded values, `x:CompileBindings="False"`, non-virtualized lists, missing `AutomationProperties`, `.Result` in command bodies)
  - Verification checklist rewritten to the 10-item Avalonia checklist (compiled bindings, zero binding warnings, Tab order, automation names, four states, both theme variants, token usage, accessibility-tool pass, Headless test, cold-start)
  - Pointer added to the companion **avalonia-dev** plugin (`/avalonia-review`) for structural reviews
- **What was preserved conceptually** (not verbatim): component-architecture framing, composition-over-configuration principle, loading/error/empty pattern, design-system-adherence framing, avoid-AI-aesthetic framing, four-tier focus on keyboard / ARIA-equivalent / focus management / color, responsive-by-default principle
- **Downstream patches** (applied after the initial sync; not tracked against upstream):
  - **2026-04-19** (plugin v1.0.3) — Version Awareness section now includes side-by-side XAML examples contrasting Avalonia 11 (`x:CompileBindings="True"` is opt-in) and Avalonia 12 (compiled bindings default-on, `x:DataType` still required for type checking). Added a "declare both on every root regardless of version" default posture so copy/paste between v11 and v12 projects is safe.
- **License**: MIT © 2025 Addy Osmani — see [`../../LICENSES/agent-skills-MIT.txt`](../../LICENSES/agent-skills-MIT.txt)
