初めに
アクセシビリティは開発で無視されがちですが、やり方がわかれば、アクセシビリティに配慮するのはそこまで難しいことではありません。
この記事ではアクセシビリティに配慮したアプリの作り方を例を交えながら説明します。
アクセシビリティに関して
アクセシビリティの定義
この記事で言うアクセシビリティに配慮するというのは、スクリーンリーダー(TalkBack / VoiceOver)による読み上げと操作に対応することを指します。
正確に言えば、認識しやすい色の使い方や小さすぎない文字サイズなどもアクセシビリティに含まれますが、デザインに関わる部分はここでは取り上げません。
アクセシビリティの基本原則
1. 説明の文字がないかつ意味を持つ要素に対しては説明ラベルを指定する
説明の文字がないですが、その要素自体は意味を持つ場合は説明ラベル
- Android: contentDescription
- iOS: label / accessibilityLabel
を指定すべきです。
注意する必要があるのは、要素自体が意味を持たない場合は指定すべきではありません。全ての画像が見えないと仮定し、UI を全部文字にするのを想像するとわかりやすいかもしれません。
また、説明の内容も要素自体の説明ではなく要素の意味の説明です。例:「送信ボタン」ではなく「送信」。
(関連記事: お前らはまだ img タグの alt 属性の付け方を間違っている)
Example
- UI 中の画像に対しては説明ラベルを指定すべき
ラベル名 | 送信 | コメント数 10 |
---|---|---|
- |
- UI 中の画像に対しては説明ラベルを指定すべきではない
状況 | 隣に説明の文字が既にある | 画像自体は意味を持たない |
---|---|---|
- |
Code Example
- Jetpack Compose
Button(onClick = {}) {
Icon(painter = Icons.Default.Send, contentDescription = "送信")
}
Button(onClick = {}) {
Icon(painter = Icons.Default.Send, contentDescription = null)
Text("送信")
}
- SwiftUI
Button(action: {}) {
Image("ic_send", label: Text("送信"))
}
Button(action: {}) {
HStack {
Image(decorative: "ic_send")
Text("送信")
}
}
2. 自前で文字を描画する場合はアクセシビリティプロパティとしてテキストを指定する
標準のテキストウィジェットを使う場合は自動でテキストがアクセシビリティプロパティに設定されますが、自前で Canvas などに文字を描画する場合は手動で指定する必要があります。
Code Example
- Jetpack Compose
Canvas(
modifier = Modifier
.size(100.dp)
.semantics { text = AnnotatedString("テキスト") }
) {
drawText("テキスト")
}
- SwiftUI
Canvas { context, size in
context.draw("テキスト")
}
.frame(width: 100, height: 100)
.accessibilityLabel(Text("テキスト"))
3. 操作できるコントロールはその状態とできる操作も指定する
トグル(スイッチ)、チェックボックス、タブバー、スライダーなど、状態と操作を持つコントロールに対しては現在の状態と押すときの操作を指定すべきです。
標準のウィジェットを使う場合は自動で指定されますが、カスタムウィジェットを作る場合は手動で指定する必要があります。
- Jetpack Compose: semantics や selectable、toggleable などの Modifier
- UIKit: accessibilityValue / accessibilityTraits
- SwiftUI: accessibility から始まる Modifier
Code Example
- Jetpack Compose
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.selectable(
selected = index == selectedIndex,
role = Role.RadioButton,
onClick = { selectedIndex = index }
)
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.size(30.dp)
.background(color = Color.Blue, shape = CircleShape)
) {
if (selected) {
Icon(Icons.Default.Check, contentDescription = null, tint = Color.White)
}
}
Text("ブルー")
}
この例を TalkBack で読み上げさせると、
「未選択、ブルー、ラジオボタン、切り替えるにはダブルタップします」
になります。
Modifier.selectable
ではなく Modifier.clickable
を使うと、
「ブルー、ダブルタップできるメニューです」
になり、選択状態と押すときの操作の説明が失われます。
- SwiftUI
VStack {
ZStack {
Circle()
.fill(Color.blue)
.frame(width: 30, height: 30)
if index == selectedIndex {
Image(decorative: "ic_check")
}
}
Text("ブルー")
}
.onTapGesture {
selectedIndex = index
}
.accessibilityAddTraits(index == selectedIndex ? [.isSelected, .isButton] : .isButton)
4. 場合によってはグルーピングが必要
例えばスイッチと文字ラベル両方ある場合はそれら一つのグループとして、全体をクリックできるリストアイテムも中に個々の要素があっても一つのグループとして扱うべきです。
クリックできるアイテムの中の要素は自動でグルーピングされますが、それ以外のアイテムのグルーピングが必要な場合は手動で設定する必要があります。
Code Example 1
- Jetpack Compose
Row(modifier = Modifier.semantics(mergeDescendants = true) {}) {
Icon(Icons.Default.Comment, contentDescription = "コメント数")
Text("10")
}
- SwiftUI
HStack {
Image("ic_comment", label: Text("コメント数"))
Text("10")
}
.accessibilityElement(children: .combine)
グルーピングすることで、スクリーンリーダーは個々の要素ではなく全体にフォーカスして読み上げます。
Before | After |
---|---|
ただし、全体として扱っても、個々の要素の読み上げには間があるため、複数のテキストを繋げて読ませたい場合は、子要素のアクセシビリティプロパティをクリアして設定し直す必要があります。
Code Example 2
- Jetpack Compose
Row(modifier = Modifier.clearAndSetSemantics {
text = AnnotatedString("あなたは1番です。")
}) {
Text("あなたは")
Text("1", style = MaterialTheme.typography.h4)
Text("番です。")
}
- SwiftUI
HStack {
Text("あなたは")
Text("1")
.font(.title)
Text("番です。")
}
.accessibilityElement(children: .ignore)
.accessibilityLabel(Text("あなたは1番です。"))
また、原則 3 で話したトグル(スイッチ)など状態と操作を持つコントロールをラベルとしてのテキストと組み合わせる場合もグルーピングが必要です。
Code Example 3
Switch
の onCheckedChange
に null
渡すことで Switch
自体のアクセシビリティを無効にし、代わりに Modifier.toggleable
で Row
全体を Switch
として認識させる。Modifier.toggleable
はクリック検知をしているのでグルーピングも含めています。
- Jetpack Compose
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.toggleable(
value = checked,
role = Role.Switch,
onValueChange = { checked = it }
)
) {
Text("通知", modifier = Modifier.weight(1f))
Switch(checked = checked, onCheckedChange = null)
}
SwiftUI では Text
を Toggle
の子要素にすることで自動でグルーピングされるため省略します。
おわりに
UI を作る時に、アクセシビリティを念頭に置き、適切なラベルを指定するだけでもアプリのアクセシビリティを大きく改善できるかもしれません。
イメージが難しければ、ぜひ一度スクリーンリーダーをオンにして遊んでみてください!