Text#accessibilityStateだけじゃない!React Nativeでアクセシブルなアプリを作る代替手法とAPI活用術
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 プロパティの注意点
checked
はtrue
,false
の他に'mixed'
(部分的にチェックされている状態)も受け取ります。 - 正しい例
<Text accessibilityState={{ disabled: true }}> {/* booleanを渡す */} 無効なテキスト </Text>
- 誤った例
<Text accessibilityState={{ disabled: "true" }}> {/* 文字列を渡している */} 無効なテキスト </Text>
b. スクリーンリーダーが状態を読み上げない
accessibilityState
を設定しても、スクリーンリーダーが期待通りに状態を読み上げない場合があります。これはいくつかの原因が考えられます。
- 要素のフォーカス順序
accessibilityState
は要素がフォーカスされたときに読み上げられることが多いため、要素のフォーカス順序が意図しない場合、状態が読み上げられないことがあります。 - プラットフォーム(iOS/Android)による違い
スクリーンリーダーの挙動はOSによって異なることがあります。iOSのVoiceOverとAndroidのTalkBackでは、同じ設定でも読み上げ方がわずかに異なる場合があります。 - 他のアクセシビリティプロパティとの競合
accessibilityLabel
やaccessibilityHint
など、他のアクセシビリティプロパティとの組み合わせによっては、スクリーンリーダーの読み上げ順序や内容に影響が出ることがあります。 - 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
プロパティとの組み合わせ
- 特に、
TouchableOpacity
やPressable
など、インタラクティブな要素に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やスクリーンリーダーの実装に依存します)。
accessibilityState
のchecked
プロパティにisChecked
または'mixed'
の値を動的に設定しています。Pressable
コンポーネントのonPress
でこれらの状態を切り替えます。useState
を使用してisChecked
とisMixed
という状態変数を管理しています。
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;
accessibilityLabel
もisExpanded
の状態に応じて変更し、ユーザーが次に何ができるかを明確に伝えます。Pressable
のaccessibilityState
にexpanded: isExpanded
を設定します。これにより、スクリーンリーダーは「展開されています」または「折りたたまれています」といった情報を付加して読み上げます。isExpanded
ステートでコンテンツの表示/非表示を切り替えます。
ここでは、accessibilityState
の代替または補完となるプログラミング手法について説明します。
accessibilityLabel と accessibilityHint の活用
これは accessibilityState
と共に最も基本的なアクセシビリティプロパティです。
-
accessibilityLabel
:- 説明: 要素の目的を簡潔に説明する文字列です。スクリーンリーダーは、要素がフォーカスされたときにこのラベルを読み上げます。
Text
コンポーネントの場合、通常はその内部のテキストがデフォルトのラベルになりますが、アイコンだけのボタンなど、視覚情報から目的が分かりにくい場合に特に重要です。 accessibilityState
との関連:accessibilityState
が要素の「状態」(例: 無効、選択済み)を伝えるのに対し、accessibilityLabel
は要素の「目的」を伝えます。これらを組み合わせることで、「(目的)が(状態)です」といった形で、より明確な情報を提供できます。- 例:
この場合、<Text accessibilityLabel={isButtonEnabled ? "購入ボタン" : "購入ボタン(無効)"} accessibilityState={{ disabled: !isButtonEnabled }} > 購入 </Text>
accessibilityLabel
で「購入ボタン」と「購入ボタン(無効)」を切り替えることで、accessibilityState
のdisabled
と同じ情報を別の形で提供できます。ただし、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
で伝える。 - 要素の目的:
accessibilityLabel
とaccessibilityRole
で伝える。