.NET MAUIアクセシビリティ実践ガイド:SemanticProperties・スクリーンリーダー対応・テスト戦略

.NET MAUIアプリのアクセシビリティを実践的に解説。SemanticPropertiesの使い方からスクリーンリーダー対応、WCAG準拠のテスト戦略まで、すぐに使えるコード例とベストプラクティスを紹介します。

.NET MAUIアクセシビリティ実装ガイド2026

はじめに:モバイルアプリのアクセシビリティ、なぜ今こそ取り組むべきなのか

世界保健機関(WHO)の推計では、世界人口の約15%——つまり10億人以上が何らかの障害を持って生活しています。正直なところ、この数字を初めて見たときはかなり衝撃を受けました。

日本国内でも状況は動いています。障害者差別解消法の改正により、2024年4月から民間事業者にも合理的配慮の提供が義務化されました。モバイルアプリのアクセシビリティ対応は、もはや「やれたらいいな」ではなく、法的義務でありビジネス上の必須要件です。

.NET MAUI(Multi-platform App UI)は、クロスプラットフォーム開発において豊富なアクセシビリティAPIを提供してくれています。この記事では、SemanticProperties、AutomationProperties、SemanticScreenReader、各プラットフォームのスクリーンリーダー対応、そしてテスト戦略まで、実践的なコード例を交えて解説していきます。

それでは、さっそく見ていきましょう。

WCAGガイドラインとモバイルアプリの関係

Web Content Accessibility Guidelines(WCAG)2.1は、もともとウェブコンテンツ向けの国際標準ですが、その4つの基本原則はモバイルアプリにもそのまま当てはまります。

  • 知覚可能(Perceivable):すべてのユーザーが情報とUIコンポーネントを認識できること
  • 操作可能(Operable):すべてのユーザーがUIコンポーネントとナビゲーションを操作できること
  • 理解可能(Understandable):情報とUIの操作が理解しやすいこと
  • 堅牢(Robust):支援技術を含む幅広いユーザーエージェントで正しく解釈できること

.NET MAUIでは、これらの原則を実現するためにSemanticPropertiesという統一的なAPIが用意されています。各プラットフォームのネイティブアクセシビリティAPIに自動でマッピングしてくれるので、一度の実装で複数プラットフォームに対応できるのが大きなメリットです。

SemanticProperties:.NET MAUIアクセシビリティの基盤

SemanticPropertiesは、.NET MAUIにおけるアクセシビリティ実装の推奨アプローチです。添付プロパティとして任意のUI要素に設定でき、ネイティブのアクセシビリティAPIに自動的にマッピングされます。ここからは、主要なプロパティを一つずつ見ていきます。

Description プロパティ

Descriptionは、スクリーンリーダーがUI要素を読み上げるときに使う短い説明テキストです。特に画像やアイコンなど、視覚的なコンテンツには必ず設定しておきたいプロパティですね。

<!-- XAML での設定 -->
<Image Source="company_logo.png"
       SemanticProperties.Description="会社のロゴ:株式会社テックリード" />

<ImageButton Source="cart_icon.png"
             SemanticProperties.Description="ショッピングカート"
             Clicked="OnCartClicked" />

<!-- 装飾的な画像の場合(スクリーンリーダーから除外) -->
<Image Source="decorative_line.png"
       AutomationProperties.IsInAccessibleTree="false" />

C#コードから設定することもできます。

// C# での設定
var profileImage = new Image { Source = "profile.png" };
SemanticProperties.SetDescription(profileImage, "ユーザープロフィール画像");

// バインディングを使った動的な設定
var productImage = new Image { Source = "product.png" };
productImage.SetValue(
    SemanticProperties.DescriptionProperty,
    $"{productName}の商品画像"
);

ここで一つ注意点があります。Labelコントロールに対してDescriptionを設定すると、そのTextプロパティの内容がスクリーンリーダーに読み上げられなくなります。Labelには通常Descriptionを設定しない方がいいでしょう。これ、けっこうハマりやすいポイントです。

Hint プロパティ

Hintは、Descriptionに補足情報を追加するためのプロパティです。ボタンやスイッチなどのインタラクティブ要素で、「これをタップしたらどうなるか」を伝えるのに使います。

<!-- ボタンにヒントを追加 -->
<ImageButton Source="like.png"
             SemanticProperties.Description="いいね"
             SemanticProperties.Hint="この投稿にいいねします" />

<!-- スイッチにヒントを追加 -->
<Switch SemanticProperties.Description="ダークモード"
        SemanticProperties.Hint="ダークモードの有効・無効を切り替えます" />

<!-- 削除ボタンの例 -->
<SwipeItem Text="削除"
           SemanticProperties.Description="項目を削除"
           SemanticProperties.Hint="この項目を完全に削除します。この操作は元に戻せません" />

Androidでの注意点:EntryコントロールのHintとPlaceholderは同じネイティブプロパティにマッピングされるので、異なる値を設定しないようにしてください。これを知らないとデバッグに時間を取られます。

HeadingLevel プロパティ

HeadingLevelは、UI要素を見出しとしてマークしてコンテンツの階層構造を定義します。SemanticHeadingLevel列挙型でNone、Level1からLevel9まで指定できます。

<!-- ページの見出し構造を定義 -->
<Label Text="設定"
       FontSize="28"
       FontAttributes="Bold"
       SemanticProperties.HeadingLevel="Level1" />

<Label Text="一般設定"
       FontSize="22"
       FontAttributes="Bold"
       SemanticProperties.HeadingLevel="Level2" />

<Label Text="通知設定" />
<Switch x:Name="notificationSwitch" />

<Label Text="表示設定"
       FontSize="22"
       FontAttributes="Bold"
       SemanticProperties.HeadingLevel="Level2" />

<Label Text="フォントサイズ" />
<Slider x:Name="fontSizeSlider" Minimum="12" Maximum="24" />

プラットフォームによる違い:Windowsでは9段階すべてがサポートされますが、AndroidとiOSでは単一の見出しレベルにまとめられます。つまり、Level1でもLevel9でもAndroid/iOSでは同じ「見出し」として扱われるんですね。Windows向けに細かい階層を作り込むかどうかは、ターゲットプラットフォーム次第です。

AutomationProperties:レガシーAPIからの移行

AutomationPropertiesは、Xamarin.Forms時代から引き継がれたアクセシビリティAPIです。.NET 8以降では非推奨になっているので、基本的にはSemanticPropertiesへの移行をおすすめします。

ただし、IsInAccessibleTreeとExcludedWithChildrenについてはSemanticPropertiesに代替がないため、今でも現役で使われています。

IsInAccessibleTree

要素をアクセシビリティツリーに含めるかどうかを制御します。装飾的な要素をスクリーンリーダーから除外するときに使いますね。

<!-- 装飾的な区切り線を除外 -->
<BoxView HeightRequest="1"
         Color="LightGray"
         AutomationProperties.IsInAccessibleTree="false" />

<!-- アニメーション要素を除外 -->
<Lottie:AnimationView
    Animation="loading.json"
    AutomationProperties.IsInAccessibleTree="false" />

ExcludedWithChildren

要素とその子要素をまるごとアクセシビリティツリーから除外します。非表示にしているレイアウト全体を一括で除外したいときに便利です。

<!-- 非表示のレイアウト全体を除外 -->
<StackLayout AutomationProperties.ExcludedWithChildren="true"
             IsVisible="false">
    <Label Text="この内容は非表示です" />
    <Button Text="操作ボタン" />
</StackLayout>

Xamarin.Formsからの移行マッピング

既存のXamarin.FormsアプリをMAUIに移行するなら、以下の対応表を参考にしてください。

// 旧:AutomationProperties.Name → 新:SemanticProperties.Description
// 旧コード
AutomationProperties.SetName(button, "送信ボタン");
// 新コード
SemanticProperties.SetDescription(button, "送信ボタン");

// 旧:AutomationProperties.HelpText → 新:SemanticProperties.Hint
// 旧コード
AutomationProperties.SetHelpText(button, "フォームを送信します");
// 新コード
SemanticProperties.SetHint(button, "フォームを送信します");

// 旧:AutomationProperties.LabeledBy → 新:SemanticProperties.Description バインディング
// 旧コード(XAML)
// <Entry AutomationProperties.LabeledBy="{x:Reference nameLabel}" />
// 新コード(XAML)
// <Entry SemanticProperties.Description="{Binding Source={x:Reference nameLabel}, Path=Text}" />

SemanticScreenReader:動的コンテンツの通知

SemanticScreenReaderは、画面上で動的に変化するコンテンツをスクリーンリーダーに伝えるためのAPIです。フォーム送信の結果、データの更新、エラーメッセージなど、ユーザーにすぐフィードバックを返したいシーンで活躍します。

using Microsoft.Maui.Accessibility;

public partial class OrderPage : ContentPage
{
    private async void OnSubmitOrderClicked(object sender, EventArgs e)
    {
        try
        {
            await _orderService.SubmitOrderAsync(currentOrder);

            // 成功メッセージをアナウンス
            SemanticScreenReader.Default.Announce("注文が正常に送信されました");

            statusLabel.Text = "注文完了";
            statusLabel.TextColor = Colors.Green;
        }
        catch (Exception ex)
        {
            // エラーメッセージをアナウンス
            SemanticScreenReader.Default.Announce(
                $"注文の送信に失敗しました:{ex.Message}"
            );

            statusLabel.Text = "エラーが発生しました";
            statusLabel.TextColor = Colors.Red;
        }
    }

    private void OnCartUpdated(int itemCount)
    {
        // カート更新の通知
        SemanticScreenReader.Default.Announce(
            $"カートが更新されました。現在{itemCount}件の商品があります"
        );
    }
}

SetSemanticFocus:フォーカス制御

SetSemanticFocusは、スクリーンリーダーのフォーカスをプログラムで特定の要素に移動させる拡張メソッドです。ページ遷移やモーダル表示の後に、ユーザーが「今どこにいるのか」をすぐ把握できるようにするために使います。

public partial class SearchResultsPage : ContentPage
{
    protected override void OnAppearing()
    {
        base.OnAppearing();

        // ページ表示時に結果サマリーにフォーカスを移動
        if (resultSummaryLabel != null)
        {
            resultSummaryLabel.SetSemanticFocus();
        }
    }

    private async void OnSearchCompleted(List<Product> results)
    {
        resultSummaryLabel.Text = $"検索結果:{results.Count}件が見つかりました";

        // 結果表示後にサマリーにフォーカス
        await Task.Delay(100); // UIの更新を待機
        resultSummaryLabel.SetSemanticFocus();

        // スクリーンリーダーにもアナウンス
        SemanticScreenReader.Default.Announce(
            $"{results.Count}件の検索結果が表示されました"
        );
    }
}

実践的なアクセシブルUIパターン

アクセシブルなフォーム設計

フォームはほとんどのモバイルアプリに欠かせない要素ですよね。ここでは、アクセシビリティを考慮したフォーム設計のパターンを紹介します。

<VerticalStackLayout Spacing="16" Padding="20">
    <Label Text="新規アカウント登録"
           FontSize="24"
           FontAttributes="Bold"
           SemanticProperties.HeadingLevel="Level1" />

    <!-- メールアドレス入力 -->
    <Label x:Name="emailLabel" Text="メールアドレス" />
    <Entry x:Name="emailEntry"
           Placeholder="[email protected]"
           Keyboard="Email"
           SemanticProperties.Description="{Binding Source={x:Reference emailLabel}, Path=Text}"
           SemanticProperties.Hint="アカウントに使用するメールアドレスを入力してください" />
    <Label x:Name="emailError"
           TextColor="Red"
           FontSize="12"
           IsVisible="false"
           SemanticProperties.Description="エラー:有効なメールアドレスを入力してください" />

    <!-- パスワード入力 -->
    <Label x:Name="passwordLabel" Text="パスワード" />
    <Entry x:Name="passwordEntry"
           IsPassword="true"
           SemanticProperties.Description="{Binding Source={x:Reference passwordLabel}, Path=Text}"
           SemanticProperties.Hint="8文字以上で、大文字・小文字・数字を含めてください" />

    <!-- パスワード強度インジケーター -->
    <ProgressBar x:Name="passwordStrength"
                 Progress="0"
                 SemanticProperties.Description="パスワード強度"
                 AutomationProperties.IsInAccessibleTree="true" />
    <Label x:Name="strengthLabel"
           Text="パスワード強度:未入力"
           FontSize="12" />

    <!-- 利用規約同意 -->
    <HorizontalStackLayout Spacing="8">
        <CheckBox x:Name="termsCheckbox"
                  SemanticProperties.Description="利用規約に同意する"
                  SemanticProperties.Hint="チェックを入れると利用規約に同意したことになります" />
        <Label Text="利用規約に同意します">
            <Label.GestureRecognizers>
                <TapGestureRecognizer Tapped="OnTermsTapped" />
            </Label.GestureRecognizers>
        </Label>
    </HorizontalStackLayout>

    <!-- 送信ボタン -->
    <Button Text="アカウントを作成"
            SemanticProperties.Hint="すべての入力項目を確認し、アカウントを作成します"
            Clicked="OnCreateAccountClicked" />
</VerticalStackLayout>

エラーバリデーションのアクセシビリティ

フォームでバリデーションエラーが起きたとき、見た目だけでなくスクリーンリーダーにもきちんと伝える必要があります。これを怠ると、視覚に障害のあるユーザーは何が起きたのかまったくわからないまま先に進めなくなってしまいます。

public partial class RegistrationPage : ContentPage
{
    private void ValidateEmail(string email)
    {
        if (string.IsNullOrWhiteSpace(email) || !email.Contains('@'))
        {
            emailError.Text = "有効なメールアドレスを入力してください";
            emailError.IsVisible = true;
            emailEntry.BackgroundColor = Color.FromArgb("#FFF0F0");

            // スクリーンリーダーにエラーをアナウンス
            SemanticScreenReader.Default.Announce(
                "エラー:有効なメールアドレスを入力してください"
            );

            // エラーメッセージにフォーカスを移動
            emailError.SetSemanticFocus();
        }
        else
        {
            emailError.IsVisible = false;
            emailEntry.BackgroundColor = Colors.Transparent;
        }
    }

    private void OnPasswordChanged(object sender, TextChangedEventArgs e)
    {
        var strength = CalculatePasswordStrength(e.NewTextValue);
        passwordStrength.Progress = strength;

        string strengthText = strength switch
        {
            < 0.25 => "非常に弱い",
            < 0.5 => "弱い",
            < 0.75 => "普通",
            _ => "強い"
        };

        strengthLabel.Text = $"パスワード強度:{strengthText}";

        // 強度変更をアナウンス
        SemanticScreenReader.Default.Announce(
            $"パスワード強度:{strengthText}"
        );
    }
}

アクセシブルなリスト表示

CollectionViewやListViewでのアクセシビリティ対応も見落としがちなポイントです。各項目に適切なアクセシビリティ情報を付与しましょう。

<CollectionView ItemsSource="{Binding Products}"
                SemanticProperties.Description="商品一覧">
    <CollectionView.ItemTemplate>
        <DataTemplate x:DataType="models:Product">
            <Frame Padding="12" Margin="4"
                   SemanticProperties.Description="{Binding AccessibleDescription}"
                   SemanticProperties.Hint="タップすると商品の詳細を表示します">
                <Grid ColumnDefinitions="80,*" ColumnSpacing="12">
                    <Image Source="{Binding ImageUrl}"
                           HeightRequest="80"
                           WidthRequest="80"
                           AutomationProperties.IsInAccessibleTree="false" />
                    <VerticalStackLayout Grid.Column="1" Spacing="4">
                        <Label Text="{Binding Name}"
                               FontAttributes="Bold"
                               FontSize="16" />
                        <Label Text="{Binding PriceFormatted}"
                               FontSize="14" />
                        <Label Text="{Binding StockStatus}"
                               FontSize="12"
                               TextColor="{Binding StockStatusColor}" />
                    </VerticalStackLayout>
                </Grid>
            </Frame>
        </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>

ViewModel側では、スクリーンリーダー用に統合された説明を生成しておくと効果的です。

public class Product : ObservableObject
{
    public string Name { get; set; }
    public decimal Price { get; set; }
    public int StockCount { get; set; }

    // スクリーンリーダー用の統合説明
    public string AccessibleDescription =>
        $"{Name}、価格{Price:C}、" +
        $"{(StockCount > 0 ? $"在庫あり(残り{StockCount}点)" : "在庫切れ")}";

    public string PriceFormatted => $"¥{Price:N0}";

    public string StockStatus =>
        StockCount > 0 ? $"在庫あり(残り{StockCount}点)" : "在庫切れ";

    public Color StockStatusColor =>
        StockCount > 0 ? Colors.Green : Colors.Red;
}

プラットフォーム別スクリーンリーダー対応

Android:TalkBack

TalkBackはAndroidの標準スクリーンリーダーです。設定 → ユーザー補助 → TalkBack から有効にできます。

  • 基本ジェスチャー:右スワイプで次の要素、左スワイプで前の要素に移動
  • ダブルタップ:選択中の要素をアクティベート
  • 見出しナビゲーション:上下スワイプで見出し間を移動

Android固有の考慮事項もいくつかあります。

// Android固有のタッチターゲットサイズの確保(最低48x48dp)
<Button Text="閉じる"
        WidthRequest="48"
        HeightRequest="48"
        Padding="12" />

// .NET MAUI 10では MauiAppCompatEditText に変更
// SelectionChanged イベントが新たにサポートされました

iOS:VoiceOver

VoiceOverはiOSの標準スクリーンリーダーで、設定 → アクセシビリティ → VoiceOver から有効にできます。

  • 基本ジェスチャー:右スワイプで次の要素、左スワイプで前の要素に移動
  • ダブルタップ:選択中の要素をアクティベート
  • ローター:上下フリックでナビゲーションモードを切り替え

iOS固有の注意点:コントロールにDescriptionを設定して、かつそのコントロールが子要素を持つ場合、スクリーンリーダーが子要素にアクセスできなくなります。この場合は子要素それぞれにアクセシビリティ情報を設定する必要があります。個人的に、この挙動には何度か悩まされました。

Windows:ナレーター

ナレーターはWindowsの標準スクリーンリーダーです。Windowsロゴキー + Ctrl + Enter で起動できます。Windowsでは9段階の見出しレベルが完全にサポートされるため、HTMLのh1〜h9に近い感覚で細かい階層構造を定義できます。

カスタムコントロールのアクセシビリティ対応

カスタムコントロールを自作する場合、アクセシビリティは最初から組み込んでおくべきです。「あとから足せばいいや」と思っていると、大抵うまくいきません。Handlerアーキテクチャを活用して、各プラットフォームのアクセシビリティAPIに適切にアクセスする方法を見てみましょう。

// カスタムレーティングコントロールの例
public class AccessibleRatingControl : ContentView
{
    public static readonly BindableProperty RatingProperty =
        BindableProperty.Create(
            nameof(Rating), typeof(int), typeof(AccessibleRatingControl),
            0, propertyChanged: OnRatingChanged);

    public int Rating
    {
        get => (int)GetValue(RatingProperty);
        set => SetValue(RatingProperty, value);
    }

    public int MaxRating { get; set; } = 5;

    public AccessibleRatingControl()
    {
        var layout = new HorizontalStackLayout { Spacing = 4 };

        for (int i = 1; i <= MaxRating; i++)
        {
            int starIndex = i;
            var starButton = new ImageButton
            {
                Source = "star_empty.png",
                WidthRequest = 44,
                HeightRequest = 44,
            };

            SemanticProperties.SetDescription(
                starButton, $"星{starIndex}");
            SemanticProperties.SetHint(
                starButton, $"評価を{starIndex}に設定します");

            starButton.Clicked += (s, e) =>
            {
                Rating = starIndex;
                SemanticScreenReader.Default.Announce(
                    $"評価が{starIndex}つ星に設定されました"
                );
            };

            layout.Children.Add(starButton);
        }

        Content = layout;
        UpdateAccessibleDescription();
    }

    private static void OnRatingChanged(
        BindableObject bindable, object oldValue, object newValue)
    {
        if (bindable is AccessibleRatingControl control)
        {
            control.UpdateStars();
            control.UpdateAccessibleDescription();
        }
    }

    private void UpdateAccessibleDescription()
    {
        SemanticProperties.SetDescription(
            this, $"評価:{MaxRating}つ中{Rating}つ星");
    }

    private void UpdateStars()
    {
        if (Content is HorizontalStackLayout layout)
        {
            for (int i = 0; i < layout.Children.Count; i++)
            {
                if (layout.Children[i] is ImageButton star)
                {
                    star.Source = i < Rating
                        ? "star_filled.png"
                        : "star_empty.png";
                }
            }
        }
    }
}

色とコントラストのアクセシビリティ

視覚障害を持つユーザーにとって、十分なカラーコントラストは本当に重要です。WCAGでは通常テキストのコントラスト比4.5:1以上、大きなテキスト(18pt以上または14ptボールド以上)では3:1以上が求められます。

// アクセシブルなカラーパレットの定義例
public static class AccessibleColors
{
    // WCAG AA準拠のコントラスト比を確保
    // 背景色:白(#FFFFFF)に対するテキスト色
    public static Color PrimaryText = Color.FromArgb("#1A1A1A");     // 比率 17.4:1
    public static Color SecondaryText = Color.FromArgb("#4A4A4A");   // 比率 9.7:1
    public static Color ErrorText = Color.FromArgb("#D32F2F");       // 比率 5.6:1
    public static Color SuccessText = Color.FromArgb("#1B5E20");     // 比率 8.9:1
    public static Color LinkText = Color.FromArgb("#0D47A1");        // 比率 9.5:1

    // ダークモード用
    // 背景色:ダーク(#121212)に対するテキスト色
    public static Color DarkPrimaryText = Color.FromArgb("#ECECEC"); // 比率 15.3:1
    public static Color DarkSecondaryText = Color.FromArgb("#B0B0B0"); // 比率 8.5:1
}

そして、色だけに頼らないことも大切です。アイコンやテキストラベルを併用して情報を伝えましょう。

<!-- 悪い例:色のみで状態を表現 -->
<BoxView Color="{Binding StatusColor}" />

<!-- 良い例:色 + アイコン + テキストで状態を表現 -->
<HorizontalStackLayout Spacing="4">
    <Image Source="{Binding StatusIcon}"
           WidthRequest="16" HeightRequest="16"
           AutomationProperties.IsInAccessibleTree="false" />
    <Label Text="{Binding StatusText}"
           TextColor="{Binding StatusColor}" />
</HorizontalStackLayout>

テキストサイズとダイナミックタイプへの対応

ユーザーがシステム設定でテキストサイズを変更したとき、アプリもそれに追従しなければなりません。固定のフォントサイズを使っていると、文字が小さすぎて読めないという声が出てきます。

<!-- 悪い例:固定ピクセルサイズ -->
<Label Text="タイトル" FontSize="16" />

<!-- 良い例:名前付きフォントサイズを使用 -->
<Label Text="タイトル" FontSize="Title" />
<Label Text="本文テキスト" FontSize="Body" />
<Label Text="キャプション" FontSize="Caption" />

<!-- レイアウトがテキストサイズの変更に適応するように -->
<Grid RowDefinitions="Auto,*"
      ColumnDefinitions="*">
    <Label Text="ヘッダー" FontSize="Header"
           Grid.Row="0" />
    <ScrollView Grid.Row="1">
        <Label Text="{Binding Content}" FontSize="Body"
               LineBreakMode="WordWrap" />
    </ScrollView>
</Grid>

アクセシビリティテスト戦略

手動テスト

アクセシビリティテストの基本は、やはり実機でのスクリーンリーダーテストです。エミュレータでもある程度はテストできますが、実機のほうが圧倒的に信頼性のある結果が得られます。

テストチェックリスト:

  1. スクリーンリーダー(TalkBack / VoiceOver)を有効にして、アプリ全体をナビゲーションする
  2. すべてのインタラクティブ要素にフォーカスが到達するか確認
  3. 読み上げ内容が適切で意味のあるものか検証
  4. 見出しナビゲーションが正しく機能するか確認
  5. フォーム入力とバリデーションエラーが適切にアナウンスされるか確認
  6. 動的に変更されるコンテンツがアナウンスされるか確認
  7. タッチターゲットが十分なサイズ(最低48×48dp)か確認
  8. テキストサイズを最大にしてもUIが崩れないか確認

このチェックリスト、最初は面倒に感じるかもしれませんが、慣れてしまえばリリース前のルーティンに組み込めます。

自動テストツール

手動テストだけではカバーしきれない部分もあるので、以下のツールも併用するといいでしょう。

  • Accessibility Insights:AndroidおよびWindowsアプリのアクセシビリティをテストできるMicrosoft製ツール
  • Accessibility Scanner:AndroidアプリのアクセシビリティをスキャンするGoogle製ツール
  • Accessibility Inspector:iOSおよびmacOSアプリのアクセシビリティを検査するApple製ツール(Xcodeに付属)
  • Visual Studio Accessibility Checker:.NET MAUIプロジェクトのアクセシビリティ問題を検出する統合ツール

UIテストでのアクセシビリティ検証

Appiumなどのテストフレームワークを使えば、アクセシビリティプロパティを自動テストに組み込むこともできます。CIに入れておけば、うっかりアクセシビリティ属性を消してしまった場合にも気づけます。

// Appium を使ったアクセシビリティテストの例
[Test]
public void SubmitButton_ShouldHaveAccessibleDescription()
{
    var submitButton = _driver.FindElement(
        MobileBy.AccessibilityId("注文を送信"));

    Assert.That(submitButton, Is.Not.Null,
        "送信ボタンがアクセシビリティIDで見つかること");
    Assert.That(submitButton.Displayed, Is.True,
        "送信ボタンが表示されていること");
}

[Test]
public void ProductList_ItemsShouldBeAccessible()
{
    var items = _driver.FindElements(
        MobileBy.ClassName("android.widget.FrameLayout"));

    foreach (var item in items)
    {
        var contentDesc = item.GetAttribute("content-desc");
        Assert.That(contentDesc, Is.Not.Null.And.Not.Empty,
            "各リスト項目にコンテンツの説明があること");
    }
}

[Test]
public void HeadingNavigation_ShouldFindAllHeadings()
{
    // Androidでの見出し検索
    var headings = _driver.FindElements(
        MobileBy.XPath(
            "//*[@class='android.widget.TextView' " +
            "and @content-desc and @heading]"));

    Assert.That(headings.Count, Is.GreaterThan(0),
        "ページに少なくとも1つの見出しがあること");
}

.NET MAUI 10のアクセシビリティ関連の改善

.NET MAUI 10では、アクセシビリティに関するいくつかの嬉しい改善が入っています。

  • MauiAppCompatEditText:AndroidのEditorおよびEntryのネイティブビューがAppCompatEditTextからMauiAppCompatEditTextに変更されました。SelectionChangedイベントのサポートが追加され、テキスト選択まわりのアクセシビリティが向上しています。
  • 診断とメトリクス:レイアウトパフォーマンスのモニタリングが可能になり、アクセシビリティに影響するレイアウトの問題を早期に検出できるようになりました。
  • MessagingCenterの内部化:MessagingCenterが内部化され、CommunityToolkit.MvvmのWeakReferenceMessengerへの移行が推奨されています。イベントベースのアクセシビリティ通知パターンを使っている場合は更新が必要です。
  • .NET Aspireテンプレート:テレメトリとサービスディスカバリを統合する新しいプロジェクトテンプレートにより、アクセシビリティの使用状況を含むアプリの可観測性が向上しています。

ベストプラクティスまとめ:押さえておきたい10のポイント

ここまでの内容を踏まえて、.NET MAUIアプリのアクセシビリティ実装で押さえておくべきポイントをまとめます。

  1. 設計段階からアクセシビリティを考慮する:後付けにすると、コストも手間も跳ね上がります。最初から組み込みましょう。
  2. SemanticPropertiesを優先的に使用する:非推奨のAutomationPropertiesではなく、SemanticPropertiesを使ってください。
  3. 意味のある説明を提供する:「ボタン1」ではなく「ショッピングカートに追加」のように、操作の意味が伝わる説明を心がけましょう。
  4. 動的コンテンツにはアナウンスを使う:SemanticScreenReader.Default.Announceで重要な状態変更をユーザーに通知してください。
  5. 見出し構造を適切に設定する:HeadingLevelプロパティでページの論理的な構造を定義しましょう。
  6. 十分なタッチターゲットサイズを確保する:インタラクティブ要素は最低48×48dp。小さすぎるボタンはユーザビリティの敵です。
  7. 色だけに依存しない:状態やエラーは色だけでなく、テキストやアイコンでも伝えましょう。
  8. テキストの拡大縮小に対応する:固定フォントサイズではなく、名前付きフォントサイズを使いましょう。
  9. 装飾的要素はアクセシビリティツリーから除外する:IsInAccessibleTree=falseで不要な読み上げを防ぎます。
  10. 実機で定期的にテストする:自動テストは補助です。実機でスクリーンリーダーを有効にしたテストを欠かさないでください。

まとめ

アクセシビリティ対応は、単なる法令遵守の話ではありません。すべてのユーザーに良い体験を届けるための、開発者としての基本的な姿勢だと思っています。

.NET MAUIはSemanticProperties、SemanticScreenReader、AutomationPropertiesといった充実したAPIを提供しており、比較的少ないコードでアクセシブルなアプリを作ることができます。この記事で紹介したパターンやベストプラクティスを参考に、まずは既存アプリにSemanticPropertiesの設定から始めてみてください。

特に.NET MAUI 10では各プラットフォーム固有の改善も入っており、以前よりもアクセシビリティ対応がしやすくなっています。完璧を目指す必要はないので、少しずつ、でも着実に改善していくことが大切です。

著者について Caleb Whitford

Caleb has shipped seven mobile apps to the App Store and Play Store over the last nine years, four of them in Xamarin and three in .NET MAUI. He spent five years at a healthcare-tech company in Boston building a clinician-facing iPad app used by roughly 14,000 nurses, then moved to a freelance practice in 2024 focused on enterprise MAUI rollouts for regulated industries. His writing tends toward the practical: CI pipelines on App Center successors, MSAL token caching across Android lifecycle resets, MAUI Blazor Hybrid in production, and the awkward seams between MAUI and native SDKs that vendors haven't gotten around to wrapping. He runs a small Discord for enterprise MAUI engineers and writes a Friday newsletter from his home in Portland, Maine.