microsoft/reactor-app
> Create WinUI 3 desktop applications using the Reactor framework — a React-inspired declarative C# projection over WinUI 3. No XAML, no data binding, no templates. This file is the legacy single-file skill — prefer the `reactor` plugin under `plugins/reactor/` (or `agentkit/plugins/reactor/` in the NuGet) for a more efficient skill-loading experience.
npx skills add https://github.com/microsoft/microsoft-ui-reactor --skill reactor-app
> Prefer the plugin. This file is preserved for environments that don't
> support the Copilot CLI / Claude plugin loading model. If you have a plugin
> SDK available, install / load the reactor plugin (under
> plugins/reactor/ in source, or agentkit/plugins/reactor/ in the NuGet).
> The plugin splits this content into focused per-skill files and is materially
> cheaper to load than this monolith.
Reactor is a React-inspired functional projection for WinUI 3. You write
functions that return lightweight element descriptions; a reconciler diffs
old vs new trees and patches real WinUI controls. State changes trigger
re-renders automatically. No XAML. No data binding. No ViewModels.
Reactor ships as a NuGet package — apps reference it as
<PackageReference Include="Microsoft.UI.Reactor" Version="…" /> (or
#:package Microsoft.UI.Reactor@… for single-file). The package carries
the framework, the analyzers, and an agent kit (signatures index +
this SKILL.md). Two paths:
| Mode | How to detect | Bootstrap |
|---|---|---|
| Selfhost — you're in a Reactor source clone (src/Reactor/Reactor.csproj exists) | The repo's local-nupkgs/ folder is the package source — see nuget.config at repo root. | Build mur once, then mur pack-local to populate local-nupkgs/Microsoft.UI.Reactor.0.0.0-local.nupkg. Re-run after framework changes. |
| Consumer — you're in an app that depends on Microsoft.UI.Reactor | No src/Reactor/ next to your project. | Nothing extra — the package already carries the analyzers and agent kit. If mur is on PATH, mur --skill and mur --api print the embedded docs. Otherwise read <package-cache>/microsoft.ui.reactor/<version>/agentkit/. |
If you're in selfhost and local-nupkgs/ is empty, restore will fail with
"package Microsoft.UI.Reactor 0.0.0-local was not found." Run mur pack-local
to fix it.
# Build the CLI; on first build the SignaturesGen project also writes
# skills/reactor.api.txt as part of its AfterBuild target.
dotnet build src/Reactor.Cli -p:Platform=ARM64
# `mur` mirrors itself to <repo>/bin/<arch>/. Add that to PATH or invoke directly.
.\bin\arm64\mur.exe pack-local
After this, any project under the clone resolves
Microsoft.UI.Reactor 0.0.0-local from local-nupkgs/ automatically (the
repo-level nuget.config configures it). A consumer outside the clone
needs a project-local nuget.config pointing at the absolute path of
<repo>/local-nupkgs/.
mur --skill, mur --api)The mur CLI ships these embedded — works from any directory:
| Command | What it prints | Source |
|---|---|---|
| mur --skill | This SKILL.md | embedded in mur |
| mur --api | The signatures index (≈70K tokens: every factory/modifier/hook/Theme token/enum, plus ctors/properties/methods/events on every public type — including static classes like ControlRegistry and ReactorApp) | embedded in mur |
| mur --regen-api | Rebuilds skills/reactor.api.txt from a freshly-built Reactor.dll (selfhost only) | rebuilds tools/Reactor.SignaturesGen |
| mur check <path> | Is the build (same exit code as dotnet build); adds one-line diagnostics with skill pointers for known REACTOR_* IDs and → try: did-you-mean suggestions | wraps MSBuild |
A consumer who doesn't have mur can read the same files directly from the
NuGet cache:
%USERPROFILE%\.nuget\packages\microsoft.ui.reactor\<version>\agentkit\
├─ SKILL.md ← this file
├─ reactor.api.txt ← signatures index
└─ skills\
├─ async.md, design.md, commanding.md, navigation.md, forms.md,
│ input.md, charts.md, dsl-reference.md, devtools.md, perf-tips.md
└─ recipes\
├─ index.md ← intent → recipe map
└─ <name>.cs ← paste-ready single-file programs
When SKILL.md or a recipe references skills/foo.md, a consumer agent
reads it from agentkit/skills/foo.md in the package cache. Selfhost
agents read it from <repo>/skills/foo.md.
skills/reactor.api.txt is a generated, alphabetized
flat list of every public Factory, Modifier, Hook, Theme token (with WinUI
resource key), and enum in Reactor. **Load this when you need to confirm a
signature.** It replaces grepping src/Reactor/Elements/*.cs and walking the
sub-skills' tables.
skills/reactor.api.txt.Run mur --api to print it. Run mur --regen-api after framework changes.
<package-cache>/microsoft.ui.reactor/<version>/agentkit/reactor.api.txt
(typically %USERPROFILE%\.nuget\packages\microsoft.ui.reactor\<version>\agentkit\reactor.api.txt).
If mur is on PATH, mur --api prints the embedded copy.
skills/recipes/ holds compilable single-file recipes for
the most common Reactor patterns. **Load a recipe instead of synthesizing
from skill prose.** See skills/recipes/index.md
for the intent → recipe map. Available today: list-add-delete, sidebar-nav,
form-with-validation, async-fetch-list, themed-card, canvas-positioning,
named-styles, calendar-multiselect.
Need a control Reactor has no built-in factory for (a vendor control or your own
custom Control)? Annotate an empty partial record and the
[GenerateReactorWrapper] source generator emits the element, descriptor,
registration, and a factory named after the control — no hand-written wrapper:
using Microsoft.UI.Reactor.Wrappers;
[GenerateReactorWrapper(typeof(Acme.Controls.RatingDial))]
public partial record RatingDialElement; // → using static …RatingDialElement; RatingDial(value: …, onValueChanged: …)
Full guidance — auto-inference, the [Wrap*] annotation cheat-sheet, imperative
lifecycle, diagnostics, and a gallery of worked examples — is in the
reactor-wrapping skill (plugins/reactor/skills/reactor-wrapping/, or
agentkit/plugins/reactor/skills/reactor-wrapping/ in the nupkg). Spec:
docs/specs/058-control-wrapper-generator.md.
mur check — fast feedback with skill pointersmur check is the build, not a separate check step. It runs dotnet build
under the hood and returns the same exit code. When mur check exits 0, the
build is green — do not re-run dotnet build to confirm. They're the same
compilation; a redundant dotnet build afterwards is wasted work.
Two enrichments over raw dotnet build:
REACTOR_* IDs.→ try: suggestions for unknown identifiers, computed againstthe live Reactor surface for the exact diagnostic.
C:\path\Program.cs:15:23 W REACTOR_DSL_001 Element produced by Select(...)… → SKILL.md gotcha #6 (.WithKey on dynamic list items)
C:\path\Program.cs:34:16 E CS1061 'ButtonElement' does not contain a definition for 'OnClick' → try: Button(label, onClick: ...) // [factory has Action onClick parameter]
<path> defaults to . and accepts a .csproj or directory. Single-file
.cs builds work but don't load analyzers — for analyzer coverage,
use a .csproj.
Trust → try: suggestions directly. Use the suggested name verbatim in your
next edit; don't grep adjacent or sibling names to second-guess it. The
suggestion has been computed against the actual Reactor surface for this exact
diagnostic. If wrong, the next mur check will tell you — that self-correcting
loop is cheaper than manual verification.
Don't introspect via [System.Reflection] to enumerate Reactor types or
members — the skill files plus mur check's did-you-mean suggestions plus
skills/reactor.api.txt cover the surface.
Workflow modes (Phase-2 ranker):
mur check — iteration mode (default). A ranker suppresses cosmetic noise(CS1591 XML doc, CS0168 unused-var, IDE0xxx style, NuGet restore chatter)
mid-iteration so the real blocker doesn't scroll off attention. **When this
exits 0, you are done** — the build is green.
mur check --final — optional pre-merge sweep. Emits the cosmeticdiagnostics suppressed during iteration (XML doc, unused locals, style
hints, nullable warnings, transient restore noise). For human code review
or a CI ship-readiness gate; not a task-completion requirement and
skipping it is fine.
mur check -- <msbuild args> — anything after a bare -- is forwardedverbatim to dotnet build (override platform, config, restore, verbosity).
| Skill | When to load |
|---|---|
| skills/async.md | Fetching data, caching, pagination, optimistic writes. UseResource, UseMutation, UseInfiniteResource, Pending. |
| skills/design.md | Any visual-styling work. Windows 11 design rules — theme tokens, High Contrast, typography, 4px grid, acrylic surfaces, accessibility. |
| skills/commanding.md | Actions that appear in multiple surfaces (menu + toolbar), need keyboard shortcuts, or need CanExecute. Command, StandardCommand, UseCommand, CommandHost. |
| skills/devtools.md | Drive a running app via mur devtools — screenshot, inspect visual tree, click/type/scroll, read hook state. Load when diagnosing visible bugs (layout, contrast) or verifying a change landed. |
| skills/navigation.md | Multi-page apps, sidebar/tab navigation, routes, deep linking, page transitions, caching. UseNavigation, NavigationHost, NavigationView, TabView. |
| skills/forms.md | Data-entry screens, validation, masked/formatted input. UseValidationContext, FormField, MaskEngine, InputFormatter. |
| skills/input.md | Gestures, pointer events, drag-and-drop, focus management. OnPan, OnPinch, OnRotate, OnDragStarting, UseElementFocus. |
| skills/charts.md | Data visualization — choosing a chart type (incl. donut, TreeChart, ForceGraph), the chart DSL, the LabelView / XTickLabelView / YTickLabelView extension points for icon-plus-text and rich labels, plus the visualization-best-practices rules to refuse to break. |
| skills/dsl-reference.md | Look up signatures — every factory, modifier, and enum in the DSL. |
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows10.0.22621.0</TargetFramework>
<Platforms>x64;ARM64</Platforms>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UseWinUI>true</UseWinUI>
<WindowsPackageType>None</WindowsPackageType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.UI.Reactor" Version="0.0.0-local" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="2.1.3" />
</ItemGroup>
</Project>
In selfhost the version is 0.0.0-local (produced by mur pack-local —
see "Which mode are you in?" above). Outside the source clone, replace it
with whatever Microsoft.UI.Reactor version you depend on.
**After dotnet new reactorapp -n <Name>, the workspace contains
exactly two source files: App.cs (entry point + initial component)
and <Name>.csproj.** There is no Program.cs and no
GlobalUsings.cs — modify App.cs in place. The .csproj does
not enable implicit usings; App.cs has its own using
directives at the top — the canonical set (System + Reactor +
Reactor.Core + Reactor.Layout + Xaml + Xaml.Controls + static
Factories) — which is the only place you add new namespaces (e.g. using System.Linq; when
you reach for .Select(...)). Don't probe the .csproj after
scaffolding unless you're adding a PackageReference or changing a
property — Restore succeeded. in the scaffold stdout is the only
confirmation you need.
Verify your edits with mur check before declaring done. From the
project directory: mur check (no arguments) runs dotnet build and
emits one compressed line per diagnostic with a → try: suggestion
when the engine recognizes the mistake; mur check --final is the
explicit "I am done iterating" sweep that emits the full diagnostic
set including suppressed iteration-mode warnings. For anything more
involved than the build/fix loop — strict-mode failures, custom
diagnostic gating, MSBuild passthrough flags — load the
reactor-build-and-check skill.
If your .csproj lives outside the Reactor clone, add a nuget.config
next to it pointing at the clone's local-nupkgs/ (absolute path):
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="reactor-local" value="C:\path\to\reactor2\local-nupkgs" />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
</packageSources>
</configuration>
Inside the clone you don't need this — the repo-level nuget.config
already configures the feed.
WindowsPackageType MUST be None (unpackaged, no App.xaml). UseWinUI
MUST be true. No XAML files of any kind.
using Microsoft.UI.Reactor;
using Microsoft.UI.Reactor.Core;
using Microsoft.UI.Reactor.Layout; // FlexDirection, FlexJustify, ... (if using Flex)
using Microsoft.UI.Xaml; // Thickness, HorizontalAlignment, VerticalAlignment
using Microsoft.UI.Xaml.Controls; // Orientation, InfoBarSeverity, etc.
using static Microsoft.UI.Reactor.Factories; // TextBlock(), Button(), VStack() bare calls
// Component root
ReactorApp.Run<MyRoot>("Title", width: 1024, height: 768);
// Inline render function
ReactorApp.Run("Title", ctx =>
{
var (msg, setMsg) = ctx.UseState("Hello!");
return VStack(TextBlock(msg), Button("Change", () => setMsg("Changed!")));
});
class Counter : Component
{
public override Element Render()
{
var (count, setCount) = UseState(0);
return VStack(
TextBlock($"Count: {count}"),
Button("+1", () => setCount(count + 1)));
}
}
var toggle = Memo(ctx =>
{
var (on, setOn) = ctx.UseState(false);
return ToggleSwitch(on, setOn);
});
// Embed:
VStack(Component<MyWidget>(), Component<AnotherWidget>())
// Typed props — use records for free structural equality:
record UserCardProps(string Name, string Role);
class UserCard : Component<UserCardProps> { ... }
Component<UserCard, UserCardProps>(new UserCardProps("Alice", "Admin"))
Memo(ctx => TextBlock("Stable")) // render once + own state
Memo(ctx => TextBlock($"Hi, {name}"), name) // re-render when deps change
Propless Component skips parent-triggered re-renders by default.
Component<TProps> skips when Equals(oldProps, newProps).
Rules: same order every render (no hooks in if/for), only from Render()
or function-component body.
| Hook | Returns | Use for |
|---|---|---|
| UseState<T>(initial) | (T, Action<T>) | Primary state |
| UseReducer<T>(initial) | (T, Action<Func<T,T>>) | State derived from previous (lists) |
| UseEffect(action, deps) | — | Side effects + cleanup |
| UseMemo<T>(factory, deps) | T | Memoized computation |
| UseCallback(action, deps) | Action | Stable callback reference |
| UseRef<T>(initial) | Ref<T> | Mutable ref across renders |
| UseObservable<T>(source) | T | Track INotifyPropertyChanged |
| UseCollection<T>(coll) | IReadOnlyList<T> | Track ObservableCollection |
| UseContext<T>(ctx) | T | Read tree-scoped ambient state |
| UsePersisted<T>(key, initial) | (T, Action<T>) | State that survives unmount |
| UseResource<T>, UseInfiniteResource, UseMutation | See skills/async.md | Async data |
var (count, setCount) = UseState(0);
var (items, updateItems) = UseReducer(new List<Todo>());
// List mutation via UseReducer (UseState with List<T> won't re-render on mutate!):
updateItems(list => [.. list, new Todo("New", false)]);
UseEffect(() => { /* mount */ }); // empty deps → once
UseEffect(() => { /* on count change */ }, count);
UseEffect(() =>
{
var timer = new Timer(...);
return () => timer.Dispose(); // cleanup
}, deps);
public static readonly Context<string> ThemeCtx = new("light");
// Provide:
VStack(...).Provide(ThemeCtx, "dark")
// Consume:
var theme = UseContext(ThemeCtx);
For the complete catalog (every factory, modifier, enum) see
skills/dsl-reference.md. The 90% cases:
// Text + layout — prefer FlexRow/FlexColumn for linear layout; they use CSS Flexbox
// semantics (grow/shrink/gap/wrap, justify-content, align-items) so the model matches
// the web. VStack/HStack remain available when you specifically want StackPanel's
// shrink-wrap behavior.
FlexColumn(children...) FlexRow(children...)
VStack(spacing, children...) HStack(spacing, children...)
TextBlock("hi") Heading("Title") SubHeading("Section") Caption("note")
// WinUI 3 type-ramp factories — map 1:1 to TitleTextBlockStyle etc.
Title("Page") Subtitle("Group") Body("paragraph") BodyStrong("bold") BodyLarge("intro")
// Card(child) factory bakes in CardBackground + 1px CardStroke + 8 radius + 16 padding.
Card(child)
Border(child).CornerRadius(8).Background(Theme.CardBackground).Padding(16)
ScrollView(VStack(...)) // modern InteractionTracker-backed; ScrollViewer(...) is the classic control if you need attached props / parallax
Grid(columns: [GridSize.Star(), GridSize.Px(200)],
rows: [GridSize.Auto, GridSize.Star()],
childA.Grid(row: 0, column: 0), childB.Grid(row: 1, column: 1))
TitleBar("App") with { Subtitle = "Home", Content = ..., RightHeader = ... }
// Controls
Button("Click", () => ...) TextBox(value, setValue, placeholder)
CheckBox(isChecked, setChecked) ToggleSwitch(on, setOn)
Slider(v, 0, 100, setV) ComboBox(items, index, setIndex)
// Strings auto-convert to TextBlockElement: VStack("A", "B") works.
isLoggedIn ? TextBlock($"Hi, {name}") : Button("Log in", onLogin)
VStack(TextBlock("always"), showExtra ? TextBlock("maybe") : null) // null filtered
When(items.Any(), () => TextBlock($"{items.Count} items"))
If(isError, () => InfoBar("Error", msg).Severity(InfoBarSeverity.Error),
() => TextBlock("OK"))
status switch {
Status.Loading => ProgressIndeterminate(),
Status.Error => TextBlock("Oops"),
Status.Success => Component<SuccessView>(),
_ => Empty()
}
ForEach(items, item => TextBlock(item.Name))
// Or LINQ: VStack(items.Select(i => TextBlock(i.Name)).ToArray())
if/for. Call them allunconditionally; conditionally use the result.
TextBlock("Hi").Bold().Margin(10) ✓ — .Bold() needs TextBlockElement.
TextBlock("Hi").Margin(10).Bold() ✗ — .Margin() returns Element.
UseReducer. UseState(new List<T>()) + list.Add()won't re-render — same reference. Use UseReducer(list => [.. list, item]).
VStack(a, condition ? b : null, c) is safe.with for init-only properties.NavigationView(items, content) with { SelectedTag = "home", IsPaneOpen = true }.
.WithKey("id") on dynamic list items. Without keys, the reconcilermatches by position and re-mounts everything on insert/reorder.
UseMemo(() => items.OrderBy(...).ToList(), items)..Flex(grow: 1) is flex-grow, not the CSS flex: 1 shorthand. Defaultbasis is auto (content size), so a growing child with large intrinsic
content (e.g. ListView with many items) overflows the container and Yoga
shrinks every sibling proportionally — heading/buttons/inputs all collapse.
Pass .Flex(grow: 1, basis: 0) (matches CSS flex: 1) or add
.Flex(shrink: 0) to each fixed-size sibling. See skills/dsl-reference.md.
using Microsoft.UI.Reactor;
using Microsoft.UI.Reactor.Core;
using Microsoft.UI.Xaml;
using static Microsoft.UI.Reactor.Factories;
ReactorApp.Run<App>("My App", width: 1200, height: 800);
class App : Component
{
public override Element Render()
{
var (page, setPage) = UseState("Home");
return Grid(
columns: [GridSize.Star()],
rows: [GridSize.Auto, GridSize.Star()],
Border(
HStack(12,
Heading("My App").VAlign(VerticalAlignment.Center),
NavBtn("Home", page, setPage),
NavBtn("Settings", page, setPage))
).Background("#f0f0f0").Padding(horizontal: 24, vertical: 12).Grid(row: 0),
Border(page switch
{
"Home" => Component<HomePage>(),
"Settings" => Component<SettingsPage>(),
_ => TextBlock("Not found")
}).Padding(24).Grid(row: 1));
}
static Element NavBtn(string label, string current, Action<string> set) =>
Button(label, () => set(label)).IsEnabled(!(label == current));
}
Reactor has three test suites. Run the one that matches what you changed.
# Unit tests — fast, no UI window (~3s)
dotnet test tests/Reactor.Tests
# Selfhost tests — real WinUI controls, in-process (~2 min)
dotnet test tests/Reactor.SelfTests
# E2E — cross-process UI Automation via winapp ui (~30s, needs the winapp CLI)
dotnet test tests/Reactor.AppTests --filter "ClassName=Reactor.AppTests.Tests.InteractiveTests"
# Everything
dotnet test Reactor.slnx
samples/Reactor.TestApp is the interactive demo, not a test runner.
dotnet runFor lightweight demos, skip the .csproj entirely. Add a file-level header:
#:package [email protected]
#:package [email protected]
#:property OutputType=WinExe
#:property TargetFramework=net10.0-windows10.0.22621.0
#:property UseWinUI=true
#:property WindowsPackageType=None
using Microsoft.UI.Reactor;
using static Microsoft.UI.Reactor.Factories;
ReactorApp.Run("Hello", ctx =>
{
var (count, setCount) = ctx.UseState(0);
return VStack(TextBlock($"Count: {count}"), Button("+1", () => setCount(count + 1)));
});
Run with dotnet run MyApp.cs -p:Platform=ARM64 (or x64). In selfhost
the version is 0.0.0-local — run mur pack-local first if the package
isn't found. Outside the clone, replace the version with the published
release you depend on.
> Always capture dotnet run output. Build errors exit with code 1.
> Read compiler output, fix, retry. Don't assume success without checking.
> Note: single-file builds do not load analyzers — for analyzer
> coverage (REACTOR_DSL_001, REACTOR_HOOKS_*, etc.), use a .csproj.
| React | Reactor |
|---|---|
| function App() {} | class App : Component { Render() } |
| useState(0) | UseState(0) |
| useReducer | UseReducer(initial) — updater is Func<T,T> |
| useEffect(() => {}, [dep]) | UseEffect(() => {}, dep) |
| useMemo(() => val, [dep]) | UseMemo(() => val, dep) |
| <div> | FlexColumn() / FlexRow() / Border() (prefer over VStack/HStack) |
| <span>text</span> | TextBlock("text") |
| <button onClick={fn}> | Button("label", fn) |
| <input value={v} onChange={fn}> | TextBox(v, fn) |
| {cond && <X/>} | cond ? X() : null |
| {items.map(i => <X/>)} | items.Select(i => X()).ToArray() |
| <Component /> | Component<MyComponent>() |
| createContext + useContext | Context<T> + .Provide() + UseContext() |
| React Query useQuery / useMutation | UseResource / UseMutation — see skills/async.md |
| className="..." | .Set(el => ...) for native access |
| display: flex / flex-grow: 1 | Flex() / .Flex(grow: 1) |
| style={{margin: 10}} | .Margin(10) |
| JSX | C# calls + using static Factories |
Take microsoft/reactor-app from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.