Text#accessibilityStateだけじゃない!React Nativeでアクセシブルなアプリを作る代替手法とAPI活用術

2025-06-06

React NativeにおけるTextコンポーネントのaccessibilityStateプロパティは、支援技術(スクリーンリーダーなど)を使用するユーザーに対して、そのコンポーネントの現在の状態を伝えるためのものです。

これは、視覚的な情報に頼ることができないユーザーがアプリケーションを操作する上で非常に重要になります。例えば、ボタンが「無効」になっているのか、「選択済み」なのかといった状態を音声で伝えることができます。

accessibilityStateはオブジェクトとして設定され、以下のプロパティを持つことができます。

  • expanded: boolean型。展開可能な要素(アコーディオンなど)が現在展開されているか、折りたたまれているかを示します。
    • 例: { expanded: true }
  • busy: boolean型。要素が現在処理中であるかどうかを示します。
    • 例: { busy: true }
  • checked: booleanまたは'mixed'型。チェック可能な要素(チェックボックスなど)の状態を示します。'mixed'は部分的にチェックされている状態を表します。
    • 例: { checked: true }, { checked: false }, { checked: 'mixed' }
  • selected: boolean型。選択可能な要素が現在選択されているかどうかを示します。
    • 例: { selected: true }
  • disabled: boolean型。要素が無効になっているかどうかを示します。
    • 例: { disabled: true }

使用例

例えば、無効化されたテキストを表示する場合、以下のように設定できます。

<Text accessibilityState={{ disabled: true }} style={{ color: 'gray' }}>
  このボタンは現在無効です
</Text>
  • WAI-ARIAの概念: WebコンテンツのアクセシビリティガイドラインであるWAI-ARIA(Web Accessibility Initiative - Accessible Rich Internet Applications)の概念に基づいており、Webアクセシビリティのベストプラクティスに沿っています。
  • ユーザー体験の改善: ユーザーが操作に迷うことなく、アプリケーションをスムーズに利用できるようになります。
  • アクセシビリティの向上: スクリーンリーダーを使用するユーザーが、UI要素の現在の状態を正確に理解できるようになります。これにより、アプリケーションの使いやすさが大幅に向上します。


よくあるエラーと問題

a. プロパティの型間違い (TypeError)

accessibilityState はオブジェクトを受け取りますが、その中の各プロパティ(disabled, selected, checked, busy, expanded)には特定の型(booleanまたは'mixed')が必要です。誤った型を渡すと、TypeError や予期せぬ動作が発生することがあります。

  • checked プロパティの注意点
    checkedtrue, false の他に 'mixed' (部分的にチェックされている状態)も受け取ります。
  • 正しい例
    <Text accessibilityState={{ disabled: true }}> {/* booleanを渡す */}
      無効なテキスト
    </Text>
    
  • 誤った例
    <Text accessibilityState={{ disabled: "true" }}> {/* 文字列を渡している */}
      無効なテキスト
    </Text>
    

b. スクリーンリーダーが状態を読み上げない

accessibilityState を設定しても、スクリーンリーダーが期待通りに状態を読み上げない場合があります。これはいくつかの原因が考えられます。

  • 要素のフォーカス順序
    accessibilityState は要素がフォーカスされたときに読み上げられることが多いため、要素のフォーカス順序が意図しない場合、状態が読み上げられないことがあります。
  • プラットフォーム(iOS/Android)による違い
    スクリーンリーダーの挙動はOSによって異なることがあります。iOSのVoiceOverとAndroidのTalkBackでは、同じ設定でも読み上げ方がわずかに異なる場合があります。
  • 他のアクセシビリティプロパティとの競合
    accessibilityLabelaccessibilityHint など、他のアクセシビリティプロパティとの組み合わせによっては、スクリーンリーダーの読み上げ順序や内容に影響が出ることがあります。
  • accessible={false} の設定
    親要素に accessible={false} が設定されている場合、子要素のアクセシビリティ情報が無視されることがあります。Text コンポーネント自体に accessible={true} を明示的に設定する必要がある場合があります。

c. Text コンポーネントの入れ子と accessible

  • アクセシビリティを考慮した構成例
    <View accessible={true} accessibilityLabel="これは複合的な要素です">
      <Text>最初の部分</Text>
      <Text>次の部分</Text>
    </View>
    
  • 誤った例(Androidで問題)
    <Text>
      <View>一部のテキスト</View> {/* Androidではエラーになる可能性がある */}
      残りのテキスト
    </Text>
    

d. 開発環境(Web版など)での警告

React Native アプリケーションをWebにコンパイルする際に、accessibilityState プロパティが認識されないという警告が表示されることがあります。これは通常、React Native for Web の制限によるものであり、ネイティブデバイスでの動作には影響しません。

a. デバッグツールの活用

  • スクリーンリーダーの直接使用: 実際にiOSのVoiceOverやAndroidのTalkBackをオンにして、アプリケーションを操作してみることが最も重要です。開発者自身が視覚情報なしでアプリを操作し、期待通りに情報が提供されているかを確認します。
  • Android Accessibility Scanner: Androidには「Accessibility Scanner」アプリがあり、これはアプリのUIをスキャンして一般的なアクセシビリティの問題を指摘してくれます。
  • Accessibility Inspector (iOS/Xcode): Xcodeに付属している「Accessibility Inspector」を使用すると、シミュレーターまたは実機上のUI要素がアクセシビリティの観点からどのように認識されているかを確認できます。各要素の accessibilityState が正しく反映されているかを確認するのに非常に役立ちます。

b. プロパティの再確認

  • disabled: {} のようにオブジェクトを渡してしまうと、予期しないエラーになることがあります。disabled: true または disabled: false のように明示的に boolean 値を渡しましょう。
  • accessibilityState オブジェクト内の各プロパティの型が正しいか、もう一度確認してください。

c. accessible プロパティとの組み合わせ

  • 特に、TouchableOpacityPressable など、インタラクティブな要素に accessibilityState を設定する場合は、その要素自体が accessible であることが前提となります。
  • Text コンポーネントに accessibilityState を設定している場合、その Text コンポーネントがアクセシブルな要素として認識されているかを確認するため、accessible={true} を明示的に追加してみることを検討してください(ただし、通常 Text はデフォルトでアクセシブルです)。

d. 必要に応じて accessibilityLabel の併用

  • もし accessibilityState が期待通りに読み上げられない場合、一時的な解決策として accessibilityLabel に状態を含めることを検討できます。
    <Text
      accessibilityState={{ disabled: true }}
      accessibilityLabel={this.state.isDisabled ? "無効なボタン" : "有効なボタン"}
    >
      {this.state.isDisabled ? "無効" : "有効"}
    </Text>
    
    これはあくまで応急処置であり、可能であれば accessibilityState が適切に機能するように根本原因を特定・解決するのが望ましいです。

e. React Native のバージョンを確認

React Native のバージョンによっては、アクセシビリティ関連の挙動が改善されている場合があります。古いバージョンを使用している場合は、最新の安定版へのアップデートを検討することも有効です。

f. 複雑なケースでの設計見直し

  • 複数の要素の状態が絡み合う複雑なUIの場合、accessibilityState だけでは十分な情報を提供できないことがあります。その場合は、accessibilityLiveRegion を使用して動的なコンテンツの変更をスクリーンリーダーに通知したり、AccessibilityInfo API を使ってより細かくアクセシビリティイベントを制御したりすることを検討してください。


基本的な使い方

accessibilityState はオブジェクトを受け取り、その中に現在の状態を示すプロパティを設定します。

import React from 'react';
import { View, Text, StyleSheet } from 'react-native';

const BasicAccessibilityStateExample = () => {
  return (
    <View style={styles.container}>
      {/* 1. 無効なテキストの例 */}
      <Text accessibilityState={{ disabled: true }} style={styles.disabledText}>
        このテキストは無効です (スクリーンリーダーは「無効」と読み上げます)
      </Text>

      {/* 2. 有効なテキストの例(デフォルトで disabled: false と同等) */}
      <Text style={styles.normalText}>
        このテキストは有効です
      </Text>

      {/* 3. 選択状態のテキストの例 */}
      <Text accessibilityState={{ selected: true }} style={styles.selectedText}>
        このアイテムは選択済みです (スクリーンリーダーは「選択済み」と読み上げます)
      </Text>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 20,
  },
  disabledText: {
    fontSize: 18,
    color: 'gray',
    marginBottom: 10,
  },
  normalText: {
    fontSize: 18,
    color: 'black',
    marginBottom: 10,
  },
  selectedText: {
    fontSize: 18,
    color: 'blue',
    fontWeight: 'bold',
    marginBottom: 10,
  },
});

export default BasicAccessibilityStateExample;

解説

  • selected: true を設定すると、「選択済み」という情報を読み上げます。
  • disabled: true を設定することで、スクリーンリーダーは「無効」といった情報を付加してテキストを読み上げます。

状態が変化する例(チェックボックス風)

ユーザーの操作によって状態が変化する要素に accessibilityState を適用する例です。ここでは簡単なチェックボックスのような挙動を模擬します。

import React, { useState } from 'react';
import { View, Text, StyleSheet, Pressable } from 'react-native';

const CheckboxLikeExample = () => {
  const [isChecked, setIsChecked] = useState(false);
  const [isMixed, setIsMixed] = useState(false); // checked: 'mixed' の例

  const toggleChecked = () => {
    setIsChecked(prev => !prev);
    setIsMixed(false); // チェック状態になったらmixedを解除
  };

  const setMixedState = () => {
    setIsMixed(true);
    setIsChecked(false); // mixed状態になったらcheckedを解除
  };

  return (
    <View style={styles.container}>
      <Pressable
        onPress={toggleChecked}
        // checked プロパティを state に基づいて動的に設定
        accessibilityState={{
          checked: isMixed ? 'mixed' : isChecked,
        }}
        accessibilityRole="checkbox" // ロールを設定すると、スクリーンリーダーがチェックボックスとして認識しやすい
        style={({ pressed }) => [
          styles.checkboxContainer,
          pressed && styles.pressed,
          isChecked && styles.checkedContainer,
          isMixed && styles.mixedContainer,
        ]}
      >
        <Text style={styles.checkboxText}>
          {isChecked ? '' : isMixed ? '' : '☐'}
        </Text>
        <Text style={styles.checkboxLabel}>
          このアイテムをチェック
        </Text>
      </Pressable>

      <View style={styles.buttonRow}>
        <Pressable onPress={setMixedState} style={styles.actionButton}>
          <Text style={styles.buttonText}>Mix状態にする</Text>
        </Pressable>
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 20,
  },
  checkboxContainer: {
    flexDirection: 'row',
    alignItems: 'center',
    padding: 15,
    borderWidth: 1,
    borderColor: '#ccc',
    borderRadius: 8,
    marginBottom: 20,
  },
  checkedContainer: {
    backgroundColor: '#e0ffe0', // チェックされたら背景色を変更
    borderColor: 'green',
  },
  mixedContainer: {
    backgroundColor: '#fffbe0', // mixed状態の背景色
    borderColor: 'orange',
  },
  pressed: {
    opacity: 0.7,
  },
  checkboxText: {
    fontSize: 24,
    marginRight: 10,
  },
  checkboxLabel: {
    fontSize: 18,
  },
  buttonRow: {
    flexDirection: 'row',
    marginTop: 20,
  },
  actionButton: {
    backgroundColor: '#007bff',
    paddingVertical: 10,
    paddingHorizontal: 15,
    borderRadius: 5,
  },
  buttonText: {
    color: 'white',
    fontSize: 16,
  },
});

export default CheckboxLikeExample;

解説

  • accessibilityRole="checkbox" を設定することで、スクリーンリーダーがこの要素をチェックボックスとして認識し、より適切なインタラクションを提供できるようになります。
    • チェックされた状態: 「チェックボックス、チェック済み」などと読み上げられます。
    • チェックされていない状態: 「チェックボックス、チェックされていません」などと読み上げられます。
    • mixed 状態: 「チェックボックス、部分的にチェック済み」などと読み上げられます(ただし、この読み上げ方はOSやスクリーンリーダーの実装に依存します)。
  • accessibilityStatechecked プロパティに isChecked または 'mixed' の値を動的に設定しています。
  • Pressable コンポーネントの onPress でこれらの状態を切り替えます。
  • useState を使用して isCheckedisMixed という状態変数を管理しています。

expanded プロパティを使って、アコーディオンのような展開・折りたたみ可能な要素の状態を伝えます。

import React, { useState } from 'react';
import { View, Text, StyleSheet, Pressable } from 'react-native';

const ExpandableContentExample = () => {
  const [isExpanded, setIsExpanded] = useState(false);

  const toggleExpand = () => {
    setIsExpanded(prev => !prev);
  };

  return (
    <View style={styles.container}>
      <Pressable
        onPress={toggleExpand}
        // expanded プロパティを動的に設定
        accessibilityState={{ expanded: isExpanded }}
        accessibilityRole="button" // ボタンとして認識させる
        accessibilityLabel={isExpanded ? "詳細を折りたたむ" : "詳細を展開する"} // ラベルも動的に変更
        style={styles.header}
      >
        <Text style={styles.headerText}>
          詳細情報 {isExpanded ? '▲' : '▼'}
        </Text>
      </Pressable>

      {isExpanded && (
        <View style={styles.content}>
          <Text>
            これは展開されたコンテンツです。ユーザーはここで追加情報を確認できます。
            通常、このセクションは最初非表示になっています。
          </Text>
        </View>
      )}
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 20,
  },
  header: {
    width: '100%',
    padding: 15,
    backgroundColor: '#f0f0f0',
    borderBottomWidth: 1,
    borderColor: '#ccc',
    borderRadius: 5,
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
  },
  headerText: {
    fontSize: 20,
    fontWeight: 'bold',
  },
  content: {
    width: '100%',
    padding: 15,
    backgroundColor: '#fafafa',
    borderWidth: 1,
    borderColor: '#eee',
    borderRadius: 5,
    marginTop: 5,
  },
});

export default ExpandableContentExample;
  • accessibilityLabelisExpanded の状態に応じて変更し、ユーザーが次に何ができるかを明確に伝えます。
  • PressableaccessibilityStateexpanded: isExpanded を設定します。これにより、スクリーンリーダーは「展開されています」または「折りたたまれています」といった情報を付加して読み上げます。
  • isExpanded ステートでコンテンツの表示/非表示を切り替えます。


ここでは、accessibilityState の代替または補完となるプログラミング手法について説明します。

accessibilityLabel と accessibilityHint の活用

これは accessibilityState と共に最も基本的なアクセシビリティプロパティです。

  • accessibilityLabel:

    • 説明: 要素の目的を簡潔に説明する文字列です。スクリーンリーダーは、要素がフォーカスされたときにこのラベルを読み上げます。Text コンポーネントの場合、通常はその内部のテキストがデフォルトのラベルになりますが、アイコンだけのボタンなど、視覚情報から目的が分かりにくい場合に特に重要です。
    • accessibilityState との関連: accessibilityState が要素の「状態」(例: 無効、選択済み)を伝えるのに対し、accessibilityLabel は要素の「目的」を伝えます。これらを組み合わせることで、「(目的)が(状態)です」といった形で、より明確な情報を提供できます。
    • :
      <Text
        accessibilityLabel={isButtonEnabled ? "購入ボタン" : "購入ボタン(無効)"}
        accessibilityState={{ disabled: !isButtonEnabled }}
      >
        購入
      </Text>
      
      この場合、accessibilityLabel で「購入ボタン」と「購入ボタン(無効)」を切り替えることで、accessibilityStatedisabled と同じ情報を別の形で提供できます。ただし、accessibilityState の方が、支援技術が標準的な状態の変化を認識し、より適切な方法で読み上げられる可能性が高いため、可能であれば accessibilityState を優先し、accessibilityLabel はあくまで補助的な役割とすることが推奨されます。

accessibilityRole の設定

accessibilityRole は、要素の役割(ボタン、リンク、見出し、チェックボックスなど)を支援技術に伝えます。これにより、スクリーンリーダーは要素をその役割に応じた標準的な方法で扱ったり、ユーザーにその役割を伝えたりすることができます。

  • :
    {/* リンクに見えるテキスト */}
    <Text
      accessibilityRole="link"
      onPress={() => Linking.openURL('https://example.com')}
      style={{ color: 'blue', textDecorationLine: 'underline' }}
    >
      当社のウェブサイト
    </Text>
    
    {/* 見出しとして機能するテキスト */}
    <Text accessibilityRole="header" style={{ fontSize: 24, fontWeight: 'bold' }}>
      セクションタイトル
    </Text>
    
  • accessibilityState との関連: accessibilityRole は要素の基本的な分類であり、accessibilityState はその分類内の特定の状態を伝えます。例えば、accessibilityRole="checkbox"accessibilityState={{ checked: true }} を組み合わせることで、「チェックボックス、チェック済み」という明確な情報を提供できます。

accessibilityValue の活用 (範囲指定のあるコンポーネント向け)

accessibilityValue は、スライダーやプログレスバーなど、範囲を持つ要素の現在の値を伝えるためのプロパティです。accessibilityState が「有効/無効」「選択済み/未選択」といった離散的な状態を扱うのに対し、accessibilityValue は連続的な値や数値範囲を扱います。

  • :
    <Slider
      minimumValue={0}
      maximumValue={100}
      value={currentProgress}
      onValueChange={setCurrentProgress}
      accessibilityLabel="タスクの進行状況"
      // accessibilityValue を使用
      accessibilityValue={{
        min: 0,
        max: 100,
        now: currentProgress,
        text: `${currentProgress}%完了`, // これが最も優先される
      }}
    />
    
  • プロパティ: min, max, now, text
    • min: 最小値
    • max: 最大値
    • now: 現在の値
    • text: 値のテキスト表現(min, max, now よりも優先されます)

accessibilityLiveRegion の活用 (動的なコンテンツ更新向け)

accessibilityLiveRegion は Android で利用可能なプロパティで、要素の内容が動的に変更されたときに、スクリーンリーダーがその変更を自動的に読み上げるかどうかを制御します。iOS では AccessibilityInfo.announceForAccessibility() に相当する機能があります。

  • 例 (iOS と Android の両方で動的読み上げを実現):
    import React, { useState, useEffect } from 'react';
    import { View, Text, StyleSheet, Button, AccessibilityInfo, Platform } from 'react-native';
    
    const AnnounceExample = () => {
      const [statusMessage, setStatusMessage] = useState('準備完了');
    
      const updateStatus = (message) => {
        setStatusMessage(message);
        if (Platform.OS === 'ios') {
          // iOSでは明示的にアナウンス
          AccessibilityInfo.announceForAccessibility(message);
        }
      };
    
      return (
        <View style={styles.container}>
          <Text
            // Androidでは accessibilityLiveRegion を使用
            accessibilityLiveRegion="polite"
            style={styles.statusText}
          >
            現在のステータス: {statusMessage}
          </Text>
          <Button title="成功メッセージ" onPress={() => updateStatus('操作が成功しました!')} />
          <Button title="エラーメッセージ" onPress={() => updateStatus('エラーが発生しました。再試行してください。')} color="red" />
        </View>
      );
    };
    
    const styles = StyleSheet.create({
      container: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
        padding: 20,
      },
      statusText: {
        fontSize: 20,
        marginBottom: 20,
        textAlign: 'center',
      },
    });
    
    export default AnnounceExample;
    
  • 例 (Android):
    import React, { useState, useEffect } from 'react';
    import { View, Text, StyleSheet, Button } from 'react-native';
    
    const LiveRegionExample = () => {
      const [message, setMessage] = useState('');
    
      useEffect(() => {
        const timer = setTimeout(() => {
          setMessage('データが正常にロードされました!');
        }, 3000);
        return () => clearTimeout(timer);
      }, []);
    
      return (
        <View style={styles.container}>
          <Text
            // Android で動的な変更を読み上げさせる
            accessibilityLiveRegion="polite"
            style={styles.messageText}
          >
            {message}
          </Text>
          <Button
            title="メッセージをリセット"
            onPress={() => setMessage('メッセージなし')}
          />
        </View>
      );
    };
    
    const styles = StyleSheet.create({
      container: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
        padding: 20,
      },
      messageText: {
        fontSize: 20,
        marginBottom: 20,
        textAlign: 'center',
      },
    });
    
    export default LiveRegionExample;
    
  • accessibilityState との関連: accessibilityState は要素がフォーカスされたときの状態を伝えるのに役立ちますが、accessibilityLiveRegion要素がフォーカスされていなくても、内容が変更されたときにユーザーに通知したい場合に特に有効です。エラーメッセージやステータスメッセージなど、ユーザーの注意を即座に引きたい場合に利用します。
  • : 'none' (読み上げない), 'polite' (現在読み上げ中の内容が終了したら読み上げる), 'assertive' (現在読み上げ中の内容を中断してすぐに読み上げる)

AccessibilityInfo は、スクリーンリーダーが有効になっているかどうかなど、デバイスのアクセシビリティ設定に関する情報を取得したり、カスタムのアクセシビリティイベントをトリガーしたりするための API です。

  • addChangeListener() / removeChangeListener(): スクリーンリーダーの有効/無効の状態が変化したときに通知を受け取ることができます。
  • announceForAccessibility(text) (iOS): 指定されたテキストをスクリーンリーダーに即座に読み上げさせます。これは Android の accessibilityLiveRegion="assertive" に近い挙動です。
  • isScreenReaderEnabled(): スクリーンリーダーが有効かどうかを非同期で確認できます。これにより、スクリーンリーダーが有効な場合にのみ特定のアクセシビリティ動作を調整することができます。

例 (スクリーンリーダー有効時のUI調整):

import React, { useState, useEffect } from 'react';
import { View, Text, StyleSheet, AccessibilityInfo } from 'react-native';

const ScreenReaderAwareComponent = () => {
  const [screenReaderEnabled, setScreenReaderEnabled] = useState(false);

  useEffect(() => {
    // コンポーネントマウント時に現在の状態を取得
    AccessibilityInfo.isScreenReaderEnabled().then((isEnabled) => {
      setScreenReaderEnabled(isEnabled);
    });

    // スクリーンリーダーの状態変化を監視
    const subscription = AccessibilityInfo.addEventListener(
      'screenReaderChanged',
      setScreenReaderEnabled
    );

    return () => {
      // コンポーネントアンマウント時に購読解除
      subscription.remove();
    };
  }, []);

  return (
    <View style={styles.container}>
      <Text style={styles.infoText}>
        スクリーンリーダーの状態: {screenReaderEnabled ? '有効' : '無効'}
      </Text>
      {screenReaderEnabled && (
        <Text style={styles.message}>
          スクリーンリーダーが有効なため、より詳細な説明を提供します。
        </Text>
      )}
      {!screenReaderEnabled && (
        <Text style={styles.message}>
          視覚的にコンテンツを提供します。
        </Text>
      )}
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 20,
  },
  infoText: {
    fontSize: 20,
    fontWeight: 'bold',
    marginBottom: 10,
  },
  message: {
    fontSize: 16,
    textAlign: 'center',
    color: '#555',
  },
});

export default ScreenReaderAwareComponent;

Text#accessibilityState は特定の要素の状態を伝えるための直接的な方法ですが、より包括的なアクセシビリティを考慮する場合、以下の点を理解し、適切に組み合わせることが重要です。

  • デバイスのアクセシビリティ設定に応じたUI調整: AccessibilityInfo.isScreenReaderEnabled() を使用する。
  • 動的なUI変更の通知: accessibilityLiveRegion (Android) や AccessibilityInfo.announceForAccessibility() (iOS) を使用する。
  • 要素の現在の値 (数値など): accessibilityValue で伝える。
  • 要素の目的: accessibilityLabelaccessibilityRole で伝える。