一覧WB-15-2 · アクセシビリティと操作性

代替テキストとフォームのエラー表示

申込フォームを使い、画像のalt、labelと入力欄、エラー文の参照先、aria-invalid、状態通知を順に確認します。赤色へ変えるだけのエラーから、何を直せばよいか文章と対応関係で伝える形へ変更します。

説明

画像や入力欄の意味は、色や見た目だけでは十分に伝わりません。画像の代替文、入力欄の名前、補足説明、エラーの対応を文書構造や属性でも表すと、支援技術からも関係をたどれます。

申込フォームを使い、画像のalt、labelと入力欄、エラー文の参照先、aria-invalid、状態通知を順に確認します。赤色へ変えるだけのエラーから、何を直せばよいか文章と対応関係で伝える形へ変更します。

1画像に必要な代替文

申込フォームのHTML(formHTML)を開き、二つの画像のaltを確認します。

(globalThis.doc=createDocument(formHTML)用意したHTMLから文書を作る,[...doc.images].map(e=>({id:e.id,hasAlt:e.hasAttribute("alt")属性の有無,alt:e.alt代替文の内容})))

案内図は入口の情報を代替文に含め、装飾画像はalt=""です。空のaltとalt属性の欠落は同じではありません。何を伝える画像かを考え、周囲の文章と重複しすぎない代替文を付けます。

2見えるラベルと対応が切れている場合

入力欄に対応するlabel一覧を確認します。

(globalThis.email=doc.getElementById("email"),{labels:[...email.labels関連付いたlabel].map(e=>e.textContent),for:doc.getElementById("email-label").htmlForlabelが参照するID,id:email.id入力欄のID})

画面に「メールアドレス」があっても、forがwrongなので入力欄への対応がありません。見た目の近さだけでは、機械が入力欄の名前として判断できる関連付けになりません。

3labelの参照先を修正する

forをemailへ直します。

(doc.getElementById("email-label").htmlFor="email"入力欄へ関連付ける,{labels:[...email.labels修正後の対応].map(e=>e.textContent)})

メールアドレスというlabelが入力欄に対応しました。placeholderを名前の代わりにすると入力後に見えなくなるため、常に確認できるlabelを使います。

4補足説明を関連付ける

aria-describedbyでhintを参照します。

(email.setAttribute("aria-describedby","hint")補足のIDを参照,{ids:email.getAttribute("aria-describedby"),description:関連する説明文doc.getElementById("hint").textContent})

名前はlabel、補足はaria-describedbyで分けて対応させます。入力目的を短く示す名前と、詳しい入力条件を説明する文章は役割が異なります。

5色だけで不正を示す

入力欄の枠を赤くし、現在の検証状態を確認します。

(email.style.borderColor="#ff8080"枠の色だけを変更,{typeMismatch:email.validity.typeMismatch入力形式の不一致,ariaInvalid:意味上の不正表示email.getAttribute("aria-invalid"),error:doc.getElementById("error").textContent})

値には@がなく、typeMismatchはtrueです。ただしエラー文もaria-invalidもありません。色だけでは、何が不正かや直し方を十分に伝えられません。ここでは現在値を表示して観察するためreadonlyにしています。

6不正な状態と修正方法を示す

aria-invalidを付け、具体的なエラー文を表示します。

(email.setAttribute("aria-invalid","true"不正な状態を示す),doc.getElementById("error").textContent=修正方法を文章で表示"メールアドレスには@を含めてください。",email.setAttribute("aria-describedby","hint error-missing"存在しない参照も含む状態),{invalid:email.getAttribute("aria-invalid"),error:doc.getElementById("error").textContent,targets:email.getAttribute("aria-describedby").split(" ").map(id=>({id,exists:Boolean(doc.getElementById(id))}))})

エラー文はありますが、参照先のerror-missingは存在しません。ARIA属性を付けただけで完了とせず、実際のIDが存在し、正しい文章へつながることを確認します。

7エラー文の参照先を直す

hintとerrorの二つを参照させます。

(email.setAttribute("aria-describedby","hint error"実在する二つの説明を参照),email.getAttribute("aria-describedby").split(" ").map(id=>({id,text:doc.getElementById(id).textContent対応する文章})))

補足とエラー文の両方へたどれます。参照先の順序も説明を理解する流れに合わせます。aria-invalidは不正の状態を示し、describedbyは具体的な説明へ関連付ける役割です。

8修正後にエラーを解除する

アドレスを修正し、不正状態とエラー文を解除します。

(email.value="a@example.test"正しい形式へ修正,email.removeAttribute("aria-invalid")不正状態を解除,email.setAttribute("aria-describedby","hint"通常の補足へ戻す),email.style.borderColor="",doc.getElementById("error").textContent="",{valid:email.validity.valid,invalid:email.getAttribute("aria-invalid"),describedby:email.getAttribute("aria-describedby")})

値が直っても古いエラー表示を残すと混乱します。入力状態の変更に合わせてaria-invalid・説明の参照・見えるエラー文を更新します。

9完了を状態メッセージで伝える

既存のstatus領域へ完了文を入れます。

(doc.getElementById("notice").textContent="申込みを受け付けました。"既存の通知領域を更新,{role:領域の役割doc.getElementById("notice").getAttribute("role"),live:通知の優先度doc.getElementById("notice").getAttribute("aria-live"),text:doc.getElementById("notice").textContent})

role=statusとaria-live=politeで、現在の操作を急に中断させずに状態変化を伝えるための領域を用意します。メッセージを出すたびにフォーカスを奪う必要はありません。実際の読み上げ方は支援技術との組合せでも確認します。

まとめ

altの有無・内容と、画像が伝える情報を対応させます。

入力欄の名前・補足・不正状態・エラー文を別の役割として関連付けます。

修正後のエラー解除と、操作位置を奪わない状態通知も含めて設計します。

操作履歴と解説を振り返れます。

ブラウザ / 操作・表示

1
(globalThis.doc=createDocument(formHTML),[...doc.images].map(e=>({id:e.id,hasAlt:e.hasAttribute("alt"),alt:e.alt})))
[
  {
    "id": "map",
    "hasAlt": true,
    "alt": "会場入口は建物の東側"
  },
  {
    "id": "decoration",
    "hasAlt": true,
    "alt": ""
  }
]
2
(globalThis.email=doc.getElementById("email"),{labels:[...email.labels].map(e=>e.textContent),for:doc.getElementById("email-label").htmlFor,id:email.id})
{
  "labels": [],
  "for": "wrong",
  "id": "email"
}
3
(doc.getElementById("email-label").htmlFor="email",{labels:[...email.labels].map(e=>e.textContent)})
{
  "labels": [
    "メールアドレス"
  ]
}
4
(email.setAttribute("aria-describedby","hint"),{ids:email.getAttribute("aria-describedby"),description:doc.getElementById("hint").textContent})
{
  "ids": "hint",
  "description": "連絡先として使うアドレスを入力します。"
}
5
(email.style.borderColor="#ff8080",{typeMismatch:email.validity.typeMismatch,ariaInvalid:email.getAttribute("aria-invalid"),error:doc.getElementById("error").textContent})
{
  "typeMismatch": true,
  "ariaInvalid": null,
  "error": ""
}
6
(email.setAttribute("aria-invalid","true"),doc.getElementById("error").textContent="メールアドレスには@を含めてください。",email.setAttribute("aria-describedby","hint error-missing"),{invalid:email.getAttribute("aria-invalid"),error:doc.getElementById("error").textContent,targets:email.getAttribute("aria-describedby").split(" ").map(id=>({id,exists:Boolean(doc.getElementById(id))}))})
{
  "invalid": "true",
  "error": "メールアドレスには@を含めてください。",
  "targets": [
    {
      "id": "hint",
      "exists": true
    },
    {
      "id": "error-missing",
      "exists": false
    }
  ]
}
7
(email.setAttribute("aria-describedby","hint error"),email.getAttribute("aria-describedby").split(" ").map(id=>({id,text:doc.getElementById(id).textContent})))
[
  {
    "id": "hint",
    "text": "連絡先として使うアドレスを入力します。"
  },
  {
    "id": "error",
    "text": "メールアドレスには@を含めてください。"
  }
]
8
(email.value="a@example.test",email.removeAttribute("aria-invalid"),email.setAttribute("aria-describedby","hint"),email.style.borderColor="",doc.getElementById("error").textContent="",{valid:email.validity.valid,invalid:email.getAttribute("aria-invalid"),describedby:email.getAttribute("aria-describedby")})
{
  "valid": true,
  "invalid": null,
  "describedby": "hint"
}
9
(doc.getElementById("notice").textContent="申込みを受け付けました。",{role:doc.getElementById("notice").getAttribute("role"),live:doc.getElementById("notice").getAttribute("aria-live"),text:doc.getElementById("notice").textContent})
{
  "role": "status",
  "live": "polite",
  "text": "申込みを受け付けました。"
}

演習完了