本文へスキップ
すべての記事

2026年のAndroidアクセシビリティ:実際に使えるTalkBackとCompose semanticsのチェックリスト

2026年版、実践的なAndroidアクセシビリティチェックリスト——TalkBack、Compose semantics、タップ領域、そしてリリース前に毎回行うテストの手順。

MFKAPPS 1 分で読めます

Androidのアクセシビリティは、UIが「完成した」後に押し込まれる、リリース前チェックリストの一項目として扱われがちだ。この順序は逆で、それは結果にも表れる。semanticsを考えずに作った画面は後から直すのに何時間もかかるが、最初からsemanticsを意識して作った同じ画面には、ほとんど追加コストがかからない。これは私が実際に運用しているチェックリストだ——ポリシー文書ではなく、Compose UIを書くときに実際にやっていることのリストと、リリース前のテスト手順だ。

後付け修正がなぜ高くつき、最初から作ればなぜそうならないのか

Androidのスクリーンリーダーであるトークバック(TalkBack)は、ピクセルを読んでいるわけではない。Composeがあなたのアプリと一緒に生成するsemanticsツリーを読んでいる。画面を書くときにこのツリーについて一度も考えなければ、TalkBackは生の実装詳細を読み上げることになる。アイコンだけのボタンは「ボタン」と読み上げられ、装飾用の画像は「画像」と読み上げられ、3つの別々のコンポーザブルからなる1行は、1つの文ではなく3つの停止点として読み上げられる。これを後から直すのは、すべてのコンポーザブルに戻って「これは本当は何と読み上げるべきか」を問い直すことを意味し、最初から同じ問いを立てるより遅い。

解決策は、semanticsをModifierチェーンの最後に付け足す後付けの要素ではなく、コンポーネントのAPIの一部として扱うことだ。

チェックリスト

1. テキスト以外のすべてのコントロールにcontentDescriptionがある——か、まったくない

ボタンに見えるテキストがあるなら、冗長な説明を追加しないこと——TalkBackはすでにそのテキストを読み上げるので、重複する説明は二重に読み上げられてしまう。ボタンがアイコンのみの場合、説明は必須だ。

IconButton(onClick = { onDelete(item.id) }) {
    Icon(
        imageVector = Icons.Default.Delete,
        contentDescription = stringResource(R.string.delete_item, item.name)
    )
}

純粋に装飾目的の画像には、明示的にcontentDescription = nullを指定する。説明がまったくない画像は「ラベルなしの画像」と読み上げられてしまい、これは沈黙よりも悪い。

2. 関連するコンテンツをmergeDescendantsでグループ化する

タイトル、サブタイトル、バッジを含むカードは、デフォルトでは3つの別々のTalkBack停止点として読み上げられる。つまり1行を聞くのに3回のスワイプが必要になる。1つの単位として読み上げられるよう、グループをラップする。

Row(
    modifier = Modifier.semantics(mergeDescendants = true) {}
) {
    Text(title)
    Text(subtitle)
    Badge { Text(status) }
}

これはTalkBackをオンにして画面を一度スワイプすることでテストできる。論理的に1行のはずのものが2回以上のスワイプを要求するなら、統合が必要だ。

3. タップ領域はアイコンの見た目のサイズではなく48dp

パディングなしの24dpのアイコンは、24dpのタップ領域になる。これはアクセシビリティガイドラインにも、手先の器用さが低下している人にとっての基本的な使いやすさにも反する。現行のComposeにあるModifier.minimumInteractiveComponentSize()は、手動でパディングを計算せずにこれを解決してくれる——レビューで窮屈に見えるアイコンだけでなく、タップ可能なすべてのアイコンに適用すること。

4. 色だけで状態を伝えない

無効な入力欄の赤い枠線は、色覚特性を持つユーザーには見えず、TalkBackユーザーには読み上げられない。色だけに依存するすべてのシグナルを、テキストまたはアイコンの変化と組み合わせること——欄の下のエラーメッセージ、ラベル横のエラーアイコンなど。これは、素早く画面を見る晴眼のユーザーにとってもより良いUXだ——色だけの情報は、単語よりも解釈に注意力を要する。

5. クリックなしで変化するコンテンツにはライブリージョンを

ユーザーが金額を入力した後に合計が更新される場合、コンポーザブルがライブリージョンとしてマークされていない限り、TalkBackはそれに気づかない。

Text(
    text = "合計: $formattedTotal",
    modifier = Modifier.semantics { liveRegion = LiveRegionMode.Polite }
)

Politeは現在の読み上げが終わるのを待ち、Assertiveは割り込む。Assertiveは控えめに使うこと——即座の注意を要するエラーには使っても、動き続ける合計には使わない。

テストの手順

これらの問題の多くはスクリーンリーダーをオンにして初めて明らかになるため、コードレビューで実際に重要な部分の半分ほどしか捕捉できないことが多い。画面をリリースする前に、私は3つのチェックを行う。

  1. TalkBackで画面全体をスワイプする——画面をオフにし、音声だけを聞く。読み上げだけでコントロールが何をするのか分からなければ、より良い説明が必要だ。
  2. Accessibility Scanner(Googleの単体アプリ)でタップ領域のサイズとコントラスト比を確認する——どちらも自動的に検出し、該当するコンポーザブルを正確に指し示してくれる。
  3. システム設定でフォントスケールを200%にする。 この倍率でテキストが切れたり、重なったり、クリップされたりするのはバグであり、例外的なケースではない——実際のユーザーのかなりの割合が、拡大したフォントサイズを常用している。

これは一部のアプリにとって他よりも重要だ。服薬リマインダーアプリのOldSchoolは、より大きなフォントスケールやスクリーンリーダーの利用がまったく例外的ではなく、むしろ中央値に近いような高齢のユーザー層を対象としている。チェックリストをコンポーネントライブラリに一度組み込んだことで、アプリが成長していく中で画面ごとに毎回作り直す必要がなくなった。

まとめ

プロジェクトの最後に行うアクセシビリティ対応が高くつくのは、それが修正的な作業だからだ——誰かがギャップに気づき、それから各画面を個別に戻って直さなければならない。各コンポーザブルを書く過程の一部として行うアクセシビリティ対応は、ほぼ無料に近い。なぜなら「これはどう読み上げられるか」「これは1つの停止点か3つか」「タップ領域は本当に十分か」といった問いは、すでにコードを見ている最中に数秒で答えられるからだ。このチェックリストをリリース週の消火活動としてではなく、新しい画面を作るときの習慣として扱えば、それはもうチェックリストですらなくなる。