Skip to content

Latest commit

 

History

History
430 lines (359 loc) · 16 KB

File metadata and controls

430 lines (359 loc) · 16 KB

Accessibility

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

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);
    }
}

Tier 1 accessibility modifiers

Here is what each modifier does:

  • .HeadingLevel() marks an element as a heading (Level1 through Level9). Screen reader users navigate by heading, just like h1--h6 in 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

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);
    }
}

Tier 2 accessibility modifiers

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.

Accessible Form

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);
    }
}

Accessible registration form

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.

Navigation Landmarks

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);
    }
}

Navigation landmarks

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."

Heading Hierarchy

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);
    }
}

Heading hierarchy

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.

Focus Trapping

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);
    }
}

Focus trap in a modal dialog

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.

Screen Reader Announcements

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);
    }
}

Screen reader announcement

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.

Semantic Panel

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);
    }
}

Semantic panel for star rating

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.

Accessibility Scanner

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().

Roslyn Analyzers

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.

Tips

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.

Next Steps

  • 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