Building Accessible .NET MAUI Apps: SemanticProperties, Screen Readers, and WCAG Compliance

A practical guide to building accessible .NET MAUI apps — covering SemanticProperties, screen reader support for TalkBack, VoiceOver, and Narrator, WCAG compliance checklists, and working code examples.

Why Accessibility in Mobile Apps Is Not Optional

Over one billion people worldwide live with some form of disability — that's roughly 15% of the global population, and a significant chunk of your potential users. Beyond the moral case (which should be enough on its own), accessibility compliance is increasingly a legal requirement. The European Accessibility Act took full effect in June 2025, the ADA continues to expand its digital reach, and app store review processes now flag the most obvious accessibility failures.

Yet most mobile development tutorials treat accessibility as a footnote. A single paragraph about alt text, maybe a mention of color contrast, and that's it.

If you've ever tried to navigate a .NET MAUI app with TalkBack or VoiceOver turned on, you know how quickly an inaccessible app falls apart. Unlabeled buttons, chaotic focus order, form inputs that silently swallow user data without any screen reader feedback — it's a mess. I've personally shipped apps where I thought accessibility was "fine" only to discover entire workflows were unusable with a screen reader. That experience changed how I approach every project.

This guide walks through every practical aspect of building accessible .NET MAUI applications, from SemanticProperties and screen reader testing to WCAG compliance. Every section includes working code you can drop into your projects today.

Understanding SemanticProperties in .NET MAUI

Back in Xamarin.Forms, developers relied on AutomationProperties to feed information to platform accessibility APIs. .NET MAUI replaces that with SemanticProperties — a set of attached properties that map directly to native accessibility layers on Android, iOS, macOS, and Windows. If you're starting new .NET MAUI development, SemanticProperties are the way to go.

SemanticProperties.Description

The Description property gives a concise text label that a screen reader announces when the user focuses on a control. You'll use this one constantly.

<Image Source="shopping_cart.png"
       SemanticProperties.Description="Shopping cart with 3 items" />

<ImageButton Source="delete_icon.png"
             SemanticProperties.Description="Delete this item"
             Clicked="OnDeleteClicked" />

A few caveats that'll save you debugging time:

  • Don't set Description on a Label — it overrides the label's Text property and the screen reader stops reading the visible text. This one catches people all the time.
  • Avoid setting Description on Entry or Editor on Android — it breaks TalkBack's action announcements. Use Placeholder or SemanticProperties.Hint instead.
  • On iOS, setting Description on a parent element prevents VoiceOver from reaching its children. Annotate leaf elements, not containers.

SemanticProperties.Hint

The Hint property provides supplementary context about what a control does or what input is expected. Screen readers announce it after the description, giving users guidance without cluttering the primary label.

<Entry Placeholder="Enter your email"
       SemanticProperties.Hint="Used for account verification and password reset"
       Keyboard="Email" />

<Button Text="Submit"
        SemanticProperties.Description="Submit order"
        SemanticProperties.Hint="Double-tap to confirm your purchase" />

SemanticProperties.HeadingLevel

Headings let screen reader users skim content quickly — just like sighted users scan visual headings. Setting HeadingLevel on a label marks it as a structural heading, so users can jump between sections.

<Label Text="Account Settings"
       SemanticProperties.HeadingLevel="Level1"
       FontSize="24"
       FontAttributes="Bold" />

<Label Text="Notification Preferences"
       SemanticProperties.HeadingLevel="Level2"
       FontSize="18"
       FontAttributes="Bold" />

Here's a quirk worth knowing: on Android and iOS, any heading level maps to a simple boolean "is heading" flag. Windows (WinUI) supports the full nine-level hierarchy. Set headings consistently regardless — it costs nothing and future platform updates may bring full level support to mobile.

Screen Readers Across Platforms

.NET MAUI targets four major platforms, each with its own primary screen reader. Understanding the differences matters because .NET MAUI's semantic properties don't force identical behavior everywhere — they rely on each platform's native accessibility engine.

Android: TalkBack

TalkBack is Android's built-in screen reader. Users navigate by swiping right (next element) or left (previous element) and double-tap to activate. To enable TalkBack during testing:

  1. Open Settings → Accessibility → TalkBack
  2. Toggle Use TalkBack on
  3. A tutorial launches automatically on first activation

Under the hood, .NET MAUI maps SemanticProperties.Description to the native contentDescription attribute, and HeadingLevel sets the accessibilityHeading flag on API 28+.

iOS and macOS: VoiceOver

VoiceOver is Apple's screen reader for both iOS and macOS. On iOS, users navigate by swiping right/left and double-tap to activate. To enable it:

  1. Open Settings → Accessibility → VoiceOver
  2. Toggle VoiceOver on

.NET MAUI maps Description to accessibilityLabel and Hint to accessibilityHint in the iOS accessibility API. VoiceOver is generally the most mature mobile screen reader and, honestly, the strictest about proper semantic structure. If your app works well with VoiceOver, it'll probably work well everywhere.

Windows: Narrator

Narrator is the built-in screen reader on Windows. Enable it with Win + Ctrl + Enter. .NET MAUI maps semantic properties to UIA (UI Automation) properties in WinUI, and you get full heading-level support through the HeadingLevel property here.

Building Accessible Forms

Forms are where accessibility most commonly breaks down. A sighted user can glance at a label above an input field and instantly understand the relationship. A screen reader user depends entirely on proper semantic linking.

So, let's get this right.

Labeling Inputs Correctly

<VerticalStackLayout Spacing="8">
    <Label x:Name="NameLabel"
           Text="Full Name"
           SemanticProperties.HeadingLevel="Level2" />
    <Entry Placeholder="Jane Doe"
           SemanticProperties.Hint="Enter your full legal name"
           AutomationId="FullNameEntry" />

    <Label x:Name="EmailLabel"
           Text="Email Address"
           SemanticProperties.HeadingLevel="Level2" />
    <Entry Placeholder="[email protected]"
           SemanticProperties.Hint="We will send a confirmation to this address"
           Keyboard="Email"
           AutomationId="EmailEntry" />

    <Label Text="Country"
           SemanticProperties.HeadingLevel="Level2" />
    <Picker Title="Select your country"
            SemanticProperties.Description="Country selector"
            SemanticProperties.Hint="Swipe up or down to browse countries"
            AutomationId="CountryPicker" />
</VerticalStackLayout>

Announcing Validation Errors

When a form field fails validation, sighted users see a red error message. Screen reader users need to hear it. Use SemanticScreenReader.Announce to push an immediate announcement:

private void OnEmailTextChanged(object sender, TextChangedEventArgs e)
{
    if (!string.IsNullOrEmpty(e.NewTextValue) && !IsValidEmail(e.NewTextValue))
    {
        EmailErrorLabel.Text = "Please enter a valid email address";
        EmailErrorLabel.IsVisible = true;
        SemanticScreenReader.Announce("Error: Please enter a valid email address");
    }
    else
    {
        EmailErrorLabel.IsVisible = false;
    }
}

This pattern makes sure validation feedback reaches everyone, regardless of whether they can see the red text on screen.

Controlling Focus Order with SemanticOrderView

Sometimes your visual layout doesn't match the logical reading order. A card component might display an image, then a description, then a title — but the screen reader should announce the title first. The .NET MAUI Community Toolkit provides SemanticOrderView for exactly this.

First, install the toolkit:

dotnet add package CommunityToolkit.Maui

Register it in MauiProgram.cs:

builder.UseMauiCommunityToolkit();

Then wrap your layout:

<toolkit:SemanticOrderView x:Name="CardSemanticOrder"
    xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit">
    <Grid RowDefinitions="200,Auto,Auto" Padding="16">
        <Image x:Name="ProductImage"
               Source="product_hero.jpg"
               SemanticProperties.Description="Red wireless headphones"
               Grid.Row="0" />
        <Label x:Name="DescriptionLabel"
               Text="Premium noise-canceling wireless headphones"
               Grid.Row="1" />
        <Label x:Name="TitleLabel"
               Text="SoundMax Pro 500"
               FontSize="20"
               FontAttributes="Bold"
               SemanticProperties.HeadingLevel="Level2"
               Grid.Row="2" />
    </Grid>
</toolkit:SemanticOrderView>

In the code-behind, set the logical order:

public ProductCardPage()
{
    InitializeComponent();
    CardSemanticOrder.ViewOrder = new List<View>
    {
        TitleLabel,        // Announce title first
        DescriptionLabel,  // Then description
        ProductImage       // Then image alt text
    };
}

Images, Icons, and Non-Text Content

Every non-text element needs a text alternative. This is WCAG Success Criterion 1.1.1 and — I'm not exaggerating — the single most common accessibility failure in mobile apps.

Informative Images

If an image conveys information, describe its content:

<Image Source="chart_revenue_q1.png"
       SemanticProperties.Description="Revenue chart showing 23 percent growth in Q1 2026" />

Decorative Images

If an image is purely decorative, hide it from the screen reader by setting an empty description and disabling focus:

<Image Source="decorative_divider.png"
       SemanticProperties.Description=""
       IsTabStop="False"
       InputTransparent="True" />

Icon Buttons

Icon-only buttons are a super common source of unlabeled controls. Always provide a Description:

<ImageButton Source="share_icon.png"
             SemanticProperties.Description="Share this article"
             SemanticProperties.Hint="Opens the system share sheet"
             Clicked="OnShareClicked" />

Color Contrast and Dynamic Text Sizing

WCAG 2.1 Level AA requires a minimum contrast ratio of 4.5:1 for normal text and 3:1 for large text (18pt regular or 14pt bold). This matters even more on mobile than desktop because people frequently use their devices outdoors in direct sunlight.

Verifying Contrast in XAML

Avoid hardcoded colors that might look fine in dark mode but fail in light mode. Use semantic colors from your AppThemeBinding and verify both themes:

<Label Text="Important notice"
       TextColor="{AppThemeBinding Light=#1A1A1A, Dark=#F0F0F0}"
       BackgroundColor="{AppThemeBinding Light=#FFFFFF, Dark=#121212}" />

Use a contrast checker tool — the Colour Contrast Analyser is free and cross-platform — to verify your color pairs meet the 4.5:1 ratio.

Respecting System Font Size

WCAG Success Criterion 1.4.4 requires text to scale up to 200% without losing content. In .NET MAUI, avoid hardcoded pixel font sizes. Instead, use named font sizes that respect the platform's dynamic type settings:

<!-- Prefer named sizes that scale with system settings -->
<Label Text="Welcome back"
       FontSize="Title" />

<Label Text="Your account summary"
       FontSize="Subtitle" />

<Label Text="Last login: March 1, 2026"
       FontSize="Body" />

Test your app with system font size cranked to maximum on both Android (Settings → Display → Font size) and iOS (Settings → Display & Brightness → Text Size). If layouts overflow or clip text at larger sizes, rework them with scrollable containers or flexible layouts. This is one of those things that's much easier to handle early than to retrofit later.

Touch Target Sizes

Small tap targets are one of the most penalized issues in accessibility audits. WCAG 2.2 Success Criterion 2.5.8 requires a minimum target size of 24×24 CSS pixels, with the recommended size being 44×44 points on iOS and 48×48 dp on Android.

<!-- Bad: Icon is too small to tap reliably -->
<ImageButton Source="settings.png"
             WidthRequest="24"
             HeightRequest="24" />

<!-- Good: Visual icon with adequate touch target -->
<ImageButton Source="settings.png"
             WidthRequest="48"
             HeightRequest="48"
             Padding="12"
             BackgroundColor="Transparent" />

Even if your icon is visually 24×24, make sure the tappable area extends to at least 48×48 using padding or minimum size constraints. Your users' thumbs will thank you.

Accessible Navigation with Shell

.NET MAUI Shell gives you built-in accessibility support for tab bars and flyout menus. That said, there are patterns you should follow to make navigation fully accessible.

<Shell xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
       xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
       xmlns:views="clr-namespace:MyApp.Views">

    <TabBar>
        <ShellContent Title="Home"
                      Icon="home.png"
                      ContentTemplate="{DataTemplate views:HomePage}"
                      Route="home" />
        <ShellContent Title="Orders"
                      Icon="orders.png"
                      ContentTemplate="{DataTemplate views:OrdersPage}"
                      Route="orders" />
        <ShellContent Title="Profile"
                      Icon="profile.png"
                      ContentTemplate="{DataTemplate views:ProfilePage}"
                      Route="profile" />
    </TabBar>

</Shell>

Shell automatically exposes tab titles to screen readers. The Title property becomes the accessible name for each tab — so always use descriptive, action-oriented titles (not just icons) so screen reader users know where each tab leads.

When navigating programmatically, announce the destination page so users have context:

await Shell.Current.GoToAsync("//orders/details?id=42");
SemanticScreenReader.Announce("Navigated to order details");

Testing Accessibility in .NET MAUI Apps

Automated tools catch some issues, but there's no substitute for manual testing with actual screen readers. Here's a practical testing workflow that covers both approaches.

Manual Testing with Screen Readers

Budget time for manual accessibility testing in every sprint. Seriously — put it on the board. Here's what to check:

  1. Focus order: Swipe through every element on each page. Is the order logical? Are any interactive elements skipped?
  2. Announcements: Does every button, image, and input get announced with a meaningful label?
  3. Headings: Can you jump between sections using the heading navigation gesture?
  4. Forms: Can you fill out every form field, hear validation errors, and submit successfully?
  5. Dynamic content: When new content appears (a toast notification, a loading indicator), is it announced?

Platform-Specific Testing Tools

Platform Tool What It Checks
Android Accessibility Scanner Content labels, touch target sizes, contrast ratios
iOS / macOS Accessibility Inspector Semantic properties, element hierarchy, VoiceOver output
Windows Accessibility Insights UIA tree, contrast, keyboard navigation

Automated Accessibility Checks with Appium

If you already have Appium-based UI tests for your .NET MAUI app, extend them with accessibility assertions. Here's how to verify that interactive elements have accessibility labels:

[Test]
public void AllButtons_ShouldHave_AccessibilityLabels()
{
    var buttons = _driver.FindElements(MobileBy.ClassName("android.widget.Button"));

    foreach (var button in buttons)
    {
        var contentDesc = button.GetAttribute("content-desc");
        var text = button.GetAttribute("text");

        Assert.That(
            string.IsNullOrEmpty(contentDesc) && string.IsNullOrEmpty(text),
            Is.False,
            $"Button at ({button.Location.X},{button.Location.Y}) has no accessible label"
        );
    }
}

Set AutomationId on all interactive elements in your XAML — these IDs map to platform accessibility identifiers and make your elements discoverable in both Appium tests and screen readers.

WCAG Compliance Checklist for .NET MAUI

Use this checklist against every page in your app before release. Print it out, tape it to your monitor — whatever works for you.

WCAG Criterion .NET MAUI Implementation Level
1.1.1 Non-text Content SemanticProperties.Description on all images and icons A
1.3.1 Info and Relationships SemanticProperties.HeadingLevel for structural headings A
1.4.3 Contrast (Minimum) 4.5:1 ratio for text colors, verified in both themes AA
1.4.4 Resize Text Named font sizes, flexible layouts that handle 200% scaling AA
2.1.1 Keyboard All interactive elements focusable via Tab key on desktop targets A
2.4.3 Focus Order Logical XAML order or SemanticOrderView A
2.4.6 Headings and Labels Descriptive headings and input labels AA
2.5.8 Target Size 48×48 dp (Android) / 44×44 pt (iOS) minimum AA
3.3.1 Error Identification SemanticScreenReader.Announce for validation errors A
4.1.2 Name, Role, Value AutomationId and SemanticProperties on all controls A

Frequently Asked Questions

How do I test .NET MAUI accessibility without a physical device?

Android emulators support TalkBack — enable it in the emulator's Settings under Accessibility. iOS simulators have limited VoiceOver support through the Accessibility Inspector in Xcode, but full VoiceOver testing really requires a physical device. For Windows, Narrator works right on your development machine. For the most accurate results, always validate on real hardware before release.

What's the difference between AutomationProperties and SemanticProperties?

AutomationProperties is the legacy Xamarin.Forms approach that still works in .NET MAUI for backward compatibility. SemanticProperties is the modern, recommended replacement. They map more cleanly to native platform APIs and avoid the naming confusion that came from Xamarin's automation-focused terminology. If you're migrating from Xamarin.Forms, plan to switch from AutomationProperties to SemanticProperties across your codebase — it's worth the effort.

Does .NET MAUI support dynamic accessibility announcements?

Yes! The SemanticScreenReader.Announce(string message) method pushes an immediate announcement to the active screen reader. Use it for validation errors, toast messages, loading completion notices, or any dynamic content change that users need to know about but might not discover through normal navigation.

How do I hide decorative elements from screen readers?

Set SemanticProperties.Description to an empty string and mark the element as non-interactive with InputTransparent="True". This tells the platform accessibility APIs to skip the element during screen reader traversal. One thing to watch out for: hiding a parent from the accessibility tree will also hide all its children on iOS, so be careful with containers.

Is WCAG compliance legally required for mobile apps?

In many jurisdictions, yes. The ADA in the United States, the European Accessibility Act in the EU, and similar legislation in Canada, Australia, and the UK increasingly apply to mobile applications. Even where it's not strictly mandated, app store guidelines from both Apple and Google encourage accessible design, and accessibility lawsuits targeting mobile apps have risen steadily since 2020. WCAG 2.1 Level AA is the most commonly referenced standard in legal and regulatory contexts.

About the Author Editorial Team

Our team of expert writers and editors.