Reactor provides accessibility modifiers on every component. They map directly to WinUI's automation properties, so screen readers, keyboard navigation, and test tools work out of the box. Modifiers are split into two tiers based on how often you need them.
Tier 1 modifiers are the ones you use constantly: labels, headings, tab order, and keyboard shortcuts. They are applied inline on every render — no lazy allocation.
class Tier1Demo : Component
{
public override Element Render()
{
return VStack(12,
TextBlock("Account Settings")
.FontSize(24).Bold()
.HeadingLevel(AutomationHeadingLevel.Level1),
TextBlock("Profile")
.FontSize(18).SemiBold()
.HeadingLevel(AutomationHeadingLevel.Level2),
TextField("", _ => { }, placeholder: "Display name")
.AutomationName("Display name")
.TabIndex(1)
.AccessKey("N"),
Button("Save", () => { })
.AutomationName("Save profile changes")
.TabIndex(2)
.AccessKey("S")
).Padding(24);
}
}Here is what each modifier does:
.HeadingLevel()marks an element as a heading (Level1 through Level9). Screen reader users navigate by heading, just likeh1--h6in HTML..AutomationName()sets the accessible label. Use it when the visible text does not fully describe the control's purpose..TabIndex()sets the tab order. Lower values receive focus first..AccessKey()assigns an Alt+key shortcut. WinUI shows the key hint when the user presses Alt..IsTabStop()controls whether the element participates in Tab navigation at all.
Tier 2 modifiers cover supplemental information, landmarks, and visibility control. They are lazy-allocated — Reactor only creates the backing storage when you use them, keeping the common case lightweight.
class Tier2Demo : Component
{
public override Element Render()
{
return VStack(12,
TextField("", _ => { }, placeholder: "Search...")
.AutomationName("Search products")
.HelpText("Type a product name or SKU to filter results")
.Width(300),
VStack(8,
TextBlock("Revenue by Region").Bold(),
TextBlock("Bar chart placeholder").Opacity(0.5)
).FullDescription(
"Bar chart showing Q1 revenue: East $4.2M, " +
"West $3.8M, Central $2.1M")
.Padding(16).Background("#f5f5f5").CornerRadius(8),
TextBlock("Decorative divider")
.Opacity(0.2)
.AccessibilityHidden()
).Padding(24);
}
}| Modifier | Purpose |
|---|---|
.HelpText() |
Extra hint read after the name |
.FullDescription() |
Extended description for complex visuals |
.AccessibilityHidden() |
Hide decorative elements from the tree |
.AccessibilityView() |
Content, Control, or Raw visibility |
.Landmark() |
Main, Navigation, Search, Form, Custom |
.Required() |
Announces "required" for form fields |
.LiveRegion() |
Announces dynamic content changes |
The tiered design means you pay zero cost for Tier 2 on elements that only need a label and heading level.
A real form combines Tier 1 and Tier 2 modifiers. Labels, required markers, help text, landmarks, and tab order work together:
class AccessibleFormDemo : Component
{
public override Element Render()
{
var (name, setName) = UseState("");
var (email, setEmail) = UseState("");
var (agree, setAgree) = UseState(false);
var valid = !string.IsNullOrWhiteSpace(name)
&& email.Contains('@') && agree;
return VStack(12,
TextBlock("Create Account").FontSize(24).Bold()
.HeadingLevel(AutomationHeadingLevel.Level1),
TextField(name, setName, header: "Full Name")
.AutomationName("Full name").Required().TabIndex(1),
TextField(email, setEmail, header: "Email")
.AutomationName("Email address").Required().TabIndex(2)
.HelpText("We'll send a verification link"),
CheckBox(agree, setAgree, label: "I accept the terms")
.TabIndex(3),
Button("Register", () => { })
.Disabled(!valid).TabIndex(4).AccessKey("R")
).Landmark(AutomationLandmarkType.Form).Padding(24);
}
}The form container uses .Landmark(AutomationLandmarkType.Form) so screen
reader users can jump directly to it. Each field has .AutomationName() for
its label, .Required() for required fields, and .TabIndex() for
predictable keyboard order. The email field adds .HelpText() to explain
what happens after submission.
Landmarks let screen reader users jump between major page regions. Use them on your navigation bar, main content area, and search box:
class LandmarksDemo : Component
{
public override Element Render()
{
return VStack(16,
HStack(8,
Button("Home", () => { }),
Button("Products", () => { }),
Button("About", () => { })
).Landmark(AutomationLandmarkType.Navigation)
.AutomationName("Main navigation"),
VStack(12,
TextBlock("Dashboard")
.FontSize(20).Bold()
.HeadingLevel(AutomationHeadingLevel.Level1),
TextBlock("Welcome back. Here is your overview.")
).Landmark(AutomationLandmarkType.Main)
.AutomationName("Main content"),
TextField("", _ => { }, placeholder: "Search...")
.AutomationName("Site search")
.Landmark(AutomationLandmarkType.Search)
).Padding(24);
}
}WinUI supports five landmark types: Navigation, Main, Search, Form,
and Custom. Pair each landmark with .AutomationName() so screen readers
announce "Main navigation" rather than just "navigation."
A clear heading structure lets screen reader users skim your page. This pairs
well with your layout hierarchy. Use Level1 for the page
title, Level2 for sections, and Level3 for subsections:
class HeadingHierarchyDemo : Component
{
public override Element Render()
{
return VStack(12,
TextBlock("Application Settings")
.FontSize(24).Bold()
.HeadingLevel(AutomationHeadingLevel.Level1),
TextBlock("Appearance")
.FontSize(18).SemiBold()
.HeadingLevel(AutomationHeadingLevel.Level2),
TextBlock("Choose your preferred theme and font size."),
TextBlock("Notifications")
.FontSize(18).SemiBold()
.HeadingLevel(AutomationHeadingLevel.Level2),
TextBlock("Email Alerts")
.FontSize(15).SemiBold()
.HeadingLevel(AutomationHeadingLevel.Level3),
TextBlock("Configure which emails you receive.")
).Padding(24);
}
}Keep your heading levels sequential — do not skip from Level1 to Level3. Screen readers use the hierarchy to build a page outline, and gaps confuse users.
UseFocusTrap locks keyboard focus inside a container — essential for modal
dialogs and flyouts. When active, Tab and Shift+Tab cycle within the
trapped subtree and cannot escape:
class FocusTrapDemo : Component
{
public override Element Render()
{
var (showModal, setShowModal) = UseState(false);
var trap = this.UseFocusTrap(showModal);
return VStack(12,
SubHeading("Focus Trapping"),
Button("Open Modal", () => setShowModal(true)),
When(showModal, () =>
Border(
VStack(12,
TextBlock("Modal Dialog").FontSize(18).Bold(),
TextBlock("Tab/Shift+Tab stays inside this panel."),
TextField("", _ => { }, placeholder: "Name")
.TabIndex(0),
Button("Close", () => setShowModal(false))
.TabIndex(1)
).Padding(24)
).WithBorder("#888", 1)
.CornerRadius(8)
.Background("#ffffff")
.FocusTrap(trap)
)
).Padding(24);
}
}Create a FocusTrapHandle with UseFocusTrap(isActive). Apply it to a
container with .FocusTrap(handle). When isActive is true, focus wraps
within the subtree; when false, normal tab behavior resumes.
Use focus trapping for any overlay that should prevent interaction with background content: modal dialogs, confirmation sheets, and dropdown menus.
UseAnnounce sends programmatic announcements to screen readers via live
regions (WCAG 4.1.3). Use it to notify users of dynamic state changes that
are not visible in the focus path:
class AnnouncementsDemo : Component
{
public override Element Render()
{
var (count, setCount) = UseState(0);
var announce = this.UseAnnounce();
return VStack(12,
SubHeading("Screen Reader Announcements"),
Button("Save", () =>
{
setCount(count + 1);
announce.Announce($"Document saved ({count + 1} times)");
}),
Button("Error (Assertive)", () =>
announce.Announce("Connection lost!", assertive: true)),
TextBlock($"Saves: {count}").Opacity(0.6),
announce.Region // invisible live region — must be in tree
).Padding(24);
}
}Create an AnnounceHandle with UseAnnounce(). Include announce.Region
somewhere in your element tree — it renders an invisible live region. Then
call announce.Announce(message) for polite announcements (queued after
current speech) or announce.Announce(message, assertive: true) to
interrupt.
Common use cases: form submission confirmations, async operation completions, error messages, and timer-based status updates.
SemanticPanel wraps a child element to provide custom automation metadata
that Reactor components cannot expose directly (since they are C# records, not
WinUI controls with overridable automation peers):
class SemanticPanelDemo : Component
{
public override Element Render()
{
var (rating, setRating) = UseState(3);
// .Semantics() wraps the element in a SemanticPanel so
// screen readers announce it as a slider, not raw buttons
return VStack(12,
SubHeading("Star Rating (Semantic Panel)"),
HStack(4, Enumerable.Range(1, 5).Select(i =>
Button(i <= rating ? "\u2605" : "\u2606",
() => setRating(i))
.AutomationName($"{i} star{(i == 1 ? "" : "s")}")
).ToArray())
.Semantics(
role: "slider",
value: $"{rating} of 5 stars",
rangeMin: 1, rangeMax: 5, rangeValue: rating),
TextBlock($"Current: {rating}/5").Opacity(0.6)
).Padding(24);
}
}| Property | Purpose |
|---|---|
SemanticRole |
Automation role (e.g., "slider") |
SemanticValue |
Current value reported to assistive tech |
RangeMinimum / RangeMaximum |
Numeric range for range-value patterns |
RangeValue |
Current numeric position in the range |
IsReadOnly |
Whether the value can be changed |
Use SemanticPanel for custom controls like star ratings, progress
indicators, or any composite widget that needs a specific automation role
beyond what WinUI infers from its visual tree.
AccessibilityScanner is a post-reconciliation diagnostic tool that walks
the element tree and flags common accessibility issues. Run it during
development or in CI to catch violations early:
// Run AccessibilityScanner during development or CI:
//
// var diagnostics = AccessibilityScanner.Scan(rootElement);
// foreach (var d in diagnostics)
// {
// Console.WriteLine($"[{d.Severity}] {d.Message}");
// Console.WriteLine($" WCAG: {d.WcagCriterion}");
// Console.WriteLine($" Fix: {d.Fix?.Modifier}({d.Fix?.SuggestedValue})");
// }
//
// Export structured JSON for CI integration:
// AccessibilityScanner.ExportJson(diagnostics, "a11y-report.json");The scanner checks for 8 common issues:
| Check | WCAG Criterion |
|---|---|
| Icon buttons without AutomationName | 4.1.2 Name, Role, Value |
| Images without alt text | 1.1.1 Non-text Content |
| Form fields without labels | 1.3.1 Info and Relationships |
| Heading-styled text missing HeadingLevel | 1.3.1 Info and Relationships |
| Concrete brushes on interactive controls | 1.4.11 Non-text Contrast |
| Missing Main landmark | 1.3.1 Info and Relationships |
| TabIndex gaps | 2.4.3 Focus Order |
| Unresolved LabeledBy references | 1.3.1 Info and Relationships |
Each A11yDiagnostic includes a Fix suggestion with the exact modifier
and value to add. Export to JSON with AccessibilityScanner.ExportJson().
Reactor ships three compile-time accessibility analyzers that flag violations as you type in your IDE:
| Diagnostic ID | Rule |
|---|---|
REACTOR_A11Y_001 |
Icon-only Button(icon, action) calls without .AutomationName() |
REACTOR_A11Y_002 |
Image() without .AutomationName() or .AccessibilityHidden() |
REACTOR_A11Y_003 |
TextField/NumberBox/PasswordBox without header: arg or label modifier |
These analyzers run automatically when you reference the Reactor.Analyzers
package. They complement the runtime AccessibilityScanner by catching the
most common violations at compile time.
Label every interactive control. If the visible text is sufficient, WinUI
infers the name automatically. Use .AutomationName() when it is not — icon
buttons, image buttons, and controls with placeholder-only text all need it.
Use .Required() instead of asterisks. Screen readers announce "required"
automatically. Visual asterisks are invisible to assistive technology unless
you also set the automation property.
Test with Narrator. Press Win+Ctrl+Enter to launch Narrator and tab through your app. Every control should announce its name, role, and state.
Keep landmarks minimal. One Main, one Navigation, and optionally one
Search per page. Too many landmarks defeat the purpose — users cannot
quickly jump when every section is a landmark.
Set .AccessKey() on frequent actions. Alt+S for Save, Alt+N for New.
WinUI renders the key tips automatically when the user presses Alt.
- Context — previous topic: share state across the tree without prop drilling
- Localization — next topic: translate strings, format numbers/dates, and support RTL layouts
- Forms and Input — build accessible forms with labels, validation, and tab order
- Navigation — add landmarks and keyboard-navigable page structure
- Styling and Theming — ensure high-contrast themes work with your accessible controls
- WinForms Interop — accessibility bridging between WinForms and Reactor







