既知の制限事項
エンジンが評価しないもの、スキャンを実行する場所(jsdom、ブラウザー、ページ自身のスクリプト)に左右されるもの、人によるレビューが必要なものをまとめています。設計上の判断である項目には、それぞれ理由を添えています。どの項目も、スキャンで確定できないことと、対処法があればその対処法を示します。問題のない結果を信頼する前にお読みください。
対処法
| こういうとき | こうします |
|---|---|
| クリック、ホバー、データの読み込みの後にしか現れないコンテンツがある | 自分でページをその状態にしてからスキャンします。重要な状態ごとにスキャンしてください。 |
| チェックにレイアウトが必要(ターゲットサイズ、テキスト間隔、テキストの下に描画されているものに対するコントラスト) | 本物のブラウザーでスキャンします。jsdomにはレイアウトがないため(結果のengine.environment.layoutはfalse)、これらのルールはそこでは計測できる範囲が狭まるか、notApplicableを報告します。詳しくは以下の各節のとおりです。 |
| キーボードトラップのような、キーボード操作に関する要件がある | キーボードでテストします。どのスキャンもこれをカバーしません。 |
結果がcantTellである | 出現箇所のhowToCheckの手順に従って、人が判断します。uncertaintyは何が足りなかったかを、leanはページの手がかりがどちらを示しているかを表します(出力スキーマを参照)。 |
| ページがレスポンシブである | 必要な幅ごとにスキャンします。 |
| 同じページの2回の実行結果が異なる | engine.versionとengine.environment(ビューポート、まだ読み込み中のフォントや画像)を比べ、スキャンの間にページを再読み込みします。 |
| スキャンで不合格が報告されない | それは適合性の主張ではありません。WCAG適合性との対応を参照してください。 |
理由は以下の各節で説明します。
1回のスキャンで見えないもの
スキャンは、ページを1つの時点、1つの状態、1つのビューポートで読みます。読むのはDOM、その計算済みスタイル、そして本物のブラウザーではレイアウトです。人がするようにページを使うことはありません。キーボードで移動したり、メニューを開いたり、データを待ったり、サイズを変えたりはしません(ブラウザーでは、いくつかのルールが確認の間だけ要素にフォーカスを当てたりスタイルシートを追加したりして、元に戻します。次の節を参照してください)。ここに挙げる項目には、そうした時間をかけた操作が必要なので、ルールを追加しても解決できません。操作のテストか人が必要です。
- キーボードトラップの検出(WCAG 2.1.2):ページ内でフォーカスを動かし、どこに移るかを観察する必要があり、DOMを1度読むだけではできません。実現できそうな手法は検討済みですが(
ACT_RULE_MAPPING.mdのキーボードトラップの節を参照)、実装はしていません。スキャンはユーザー操作や、それによって生じる状態をたどらず、ページを一時的に変更するチェックのどれも、トラップの検出のようにページ内でフォーカスを動かしていくことはないからです。それまでの間、静的なマークアップで代わりになるものはありません。 - 拡大時のリフロー/切り取り(WCAG 1.4.10):320px相当のビューポートをシミュレートしたうえで、実際のレイアウト計測(
clientWidth/scrollWidth)が必要です。このエンジンのほかの箇所にあるCSS宣言ベースのヒューリスティックとは違い、「400%に拡大したときにコンテンツが切れるか」を静的なマークアップから推し量る手がかりはまったくありません。 - 動的な状態/操作後の状態:クリック、ホバー、非同期のデータ読み込みの後にしか存在しないもの(モーダルの中身、ドロップダウンの選択肢、フォームのバリデーションエラーのメッセージ)は、ページの現在のDOMをスキャンしても見えません。フレームワークがそれを最初からレンダリングしていれば(画面外や
hiddenでも)スキャンできます。操作後にしか存在しない場合は、スキャン前に自分でその操作を行わない限りスキャンできません(例:ボタンをクリックし、その後でスキャンする)。開いた状態をスキャンして確認できるのはその中身であって、キーボードやスクリーンリーダーの利用者がそれを開き、使い、閉じられるかどうかではありません。
環境による制限:実行方法によって変わるもの
- jsdom(Node上で動き、本物のブラウザーを使わない)にはCSSのレイアウトエンジンがありません。実際のジオメトリが必要なルール、特に
target-size-minimum(WCAG 2.5.8。実際のgetBoundingClientRect()が必要)は、素のjsdomでは推測せずにnotApplicableを報告します。これらのルールで実際の結果を得るには、本物のブラウザー上で実行してください(Puppeteer/Playwright。INTEGRATION.mdのパターン2を参照)。 - テキストが折り返すかどうかはレイアウトが必要なので、折り返さないテキストに対するテキスト間隔の検出結果はレビュー対象として報告されます。WCAG 1.4.12とその元になっているACTルールは、ソフトな折り返し位置を含むテキストにだけ適用されますが、静的なスキャンではそれを確定できません。
avoid-inline-spacingはデフォルトでテキストが折り返すものとして扱うので、基準値を下回る値を強制するふつうのケースは引き続きfailになります。レイアウトなしでも見て取れる理由で折り返しがありえない場合(テキストの折り返しが禁止されている、横スクロールする祖先の中にある固定幅の要素など)は、代わりにcantTellを報告します。それ以外の理由で折り返さないテキストは、引き続き失敗として報告されます。 - エンジンはページ自身のJavaScriptの組み込みオブジェクトの上で動作します。ページのスクリプトが、エンジンが依存する組み込みオブジェクト(
Array.prototype.filter、Map、JSON.stringify、String.prototype.toLowerCaseなど)を壊している場合、もっともらしく見えて誤っている結果を返すのではなく、それぞれを示すPAGE_BUILTINS_BROKENというコードのエラーでスキャンを止めます。バンドルを注入する前に壊されたもの(Object.keys)は、バンドルの読み込みを止めます。Object.prototypeにthenを追加するページは、ほかのページと同じようにスキャンされます。ページが置き換えたDOMのメソッド(getComputedStyle、querySelectorAll、getBoundingClientRect)は確認しません。ルールはそれらが返すものを読みます。 - 256階層より深く入れ子になったコンテンツからは、名前を求めません。たどる処理はそこで止まります(Chromeは100階層下のコンテンツから名前を与えません)。そのため、名前がそれより深い場所にしかない要素については、
button-name-present、link-name-present、メニュー項目、オプション、summary、タブ、ツールチップ、ツリー項目の名前のルール、そしてempty-headingが確認を求めます(name_contentTooDeep)。コンテンツから名前を求めるほかのルールは、そうしたコンテンツを空として読みます。 - 閉じたシャドウルート内のコンテンツは読み取れません。
attachShadow({ mode: 'closed' })や宣言的なshadowrootmode="closed"の内側にはどのスクリプトも届かないため、ブラウザーがそこから求める名前をエンジンは見られません。button-name-presentとlink-name-present、そしてメニュー項目、オプション、タブ、ツリー項目、summary、ツールチップの名前のルールは、描画はされるが読み取れるコンテンツを示さないカスタム要素から名前が来ている可能性がある要素について、不合格にする代わりに確認を求めます。組み込み要素(<span>)上の閉じたルートは、CSSで描いたアイコンのような空のものと見分けがつかないため、その上にあるボタンは引き続き不合格になります。そうしたコンポーネントが何を公開しているかは、スクリーンリーダーなどを使って人が確認してください。 - カスタム要素が
ElementInternalsを通じて設定した名前や状態は読み取れません。this.attachInternals().ariaLabel = 'Save'はどのブラウザーでも要素に名前を付けますが、どのスクリプトもそれを読み戻せません。コンポーネントがinternalsをプロパティ(_internals、internals、internals_、__internals)に保持している場合、エンジンはそこから読み取り、その内容に従って要素を合格または不合格にします。名前(または必須の状態)のないほかの定義済みカスタム要素については、名前のルールとaria-required-attrが、不合格にする代わりに確認を求めます(name_internals、state_internals)。それらはブラウザーのアクセシビリティツリーで確認するか、ページのスクリプトが動く前に、オプトインのinternalsフックをインストールしてください。各バインディング(はじめにを参照)とtest-matchersのinstallInternalsHook()、自分で用意したページならcoreのinternalsHookScript()を使います(インストールする場所はINTEGRATION.mdを参照してください)。フックはスキャンのためにすべての要素のinternalsを記録するので、internalsを一度も付けていない要素も判別できます。internalsだけで設定したロール(internals.role)は、internalsを読める場所(プロパティかフック)であれば、要素のデフォルトのロールとして読み取ります。フックなしでプライベートフィールドに保持されている場合は見えず、その要素について確認も求めません。ほぼすべてのコンポーネントがそうしたロールを持ちうるからです。組み込み要素と、定義されていないカスタム要素は、これまでどおりに判定します。 - スクリプトで追加したイベントリスナーは列挙できません。
addEventListenerで追加したリスナーや、プロパティとして設定したリスナー(el.onclick = fn)はマークアップに何も残さず、どのスクリプトも読み戻せません。そのため、キーボードのルール(click-handler-keyboard-operable、mouse-only-event-handlers)はインラインのハンドラー属性だけを判定します。スクリプトでクリックリスナーを付けたフォーカス可能なrole="button"は報告されません(フォーカスを受け取れないものについては、それでもwidget-keyboard-focusableが確認を求めます)。スクリプトで追加したキーリスナーは、インラインのonclickを正当化できません。password-paste-enabledは、インラインのonpasteが取り消したテキストを、スクリプトで追加した貼り付けリスナーが戻すかどうかを判断できません。ページのスクリプトが動く前に、オプトインのリスナーフック(listenerHookScript())をインストールしてください。ルールはフックが記録したリスナーも判定するようになり、キーリスナーが1つも見られなかった場合は、EnterキーとSpaceキーで操作できないコントロールを不合格にします。フレームワークがルート要素に付けるクリックリスナーは、何も指定していない要素のためのものなので、フックがあってもそれらの要素はマークアップで判定されます。インストールする場所は、GitHub上のINTEGRATION.mdを参照してください。 - ライブリージョンがどう埋められたかは、後からは見えません。ステータスメッセージは、支援技術が届く状態でライブリージョンが変化したときに読み上げられます(4.1.3)が、スキャンが見るのはある時点のページです。何も追加しなければ、
live-region-announcementsが判定するのはそこにあるものだけです。aria-hiddenやinertで支援技術から隠されたライブリージョン、開いているモーダルな<dialog>の背後にあるライブリージョン、aria-busyのまま残されたライブリージョンです。オプトインのライブリージョンフック(liveRegionHookScript())をインストールすると、ページが最初からテキストを入れて追加したリージョンや、表示と同時に埋めたリージョンについても確認を求め、getAnnouncements()はページが読み上げ用に提供したものを列挙します。どちらも、スクリーンリーダーが実際に何を読み上げたかは示しません。それはスクリーンリーダーによって異なります。フックはシャドウルートの中は見ません。詳細は、GitHub上のINTEGRATION.mdを参照してください。 - モーダルダイアログが開いている間に行ったスキャンは、ページではなくダイアログを見ます。
showModal()で開いた<dialog>は文書の残りの部分を不活性にし、ブラウザーは支援技術にダイアログだけを渡します。open属性やshow()で開いたダイアログは、aria-modal="true"があってもモーダルではありません。背後のページには引き続き到達でき、エンジンもそれを判定します。トップレイヤーのないjsdomでは、開いている<dialog aria-modal="true">を代わりにモーダルとみなします。そこでエンジンが判断できる中では最も近いものだからです。エンジンはこれに従います。ダイアログの背後にあるコンテンツは、ほかの非表示のコンテンツと同じようにすべてのルールの対象外になり、ページ自体の構造に関するルール(page-has-heading-one、landmark-one-main、bypass-blocks-present、region)は、スキャンからその構造が見えないためnotApplicableを報告します。背後にあるものを判断するには、ダイアログを閉じた状態でもう一度ページをスキャンしてください。独自のモーダル、つまりrole="dialog"またはrole="alertdialog"とaria-modal="true"を持つ表示中の要素も、これらの構造に関するルールをnotApplicableにします。aria-modalは何も不活性にしないので、ほかのすべてのルールは背後のページを引き続き判定します。開いたシャドウルート内のモーダルダイアログは、文書内のものと同じように扱います。閉じたシャドウルート内のものは見つけられません。aria-modal="true"のない独自のモーダルは、ページが残りの部分に付けたinertやaria-hiddenを通じてしか扱われません。これらはページを隠しますが、ダイアログが開いていることを構造に関するルールに伝えません。 <dialog>など、デフォルトのUAスタイルシートで非表示になる要素(open属性がなく、仕様上display: none)と、display:none、visibility:hidden、[hidden]、閉じた<details>で非表示になっているその他のサブツリー、そして[inert]のサブツリー(描画はされるので、コントラストやテキスト間隔など見た目に関するルールは引き続き判定します)は、デフォルトでルールの評価対象から除外されます。これは見落としではなく意図的なデフォルトです(engineOptions.includeHiddenElements: false)。非表示のコンテンツは表示されるまで支援技術からもキーボードからも到達できないので、その中のマークアップの欠陥をデフォルトで指摘すると、多くの場合ノイズになります。それでも非表示/折りたたまれたサブツリーを評価したい場合は、engineOptions.includeHiddenElements: trueを設定してください。たとえば、ダイアログが開かれる前にマークアップの欠陥(壊れたARIAのID参照など)を見つけたい場合です。このオプションと、対象になる非表示の仕組みの詳細はENGINE_OPTIONS.mdを参照してください。- 非表示のコンテンツを指す
aria-detailsは報告されません。aria-valid-attr-valueは、IDが要素を指していることは確認しますが、その要素が表示されていることは確認しません。Chromiumは、要素が非表示の間はその関係をアクセシビリティツリーから外し、表示されると戻します。そのため、必要に応じて開く詳細は開いたときに読み上げられ、WCAGもそれ以上は求めていません。支援技術が受け取るとおりに関係を確認するには、詳細を表示した状態でページをスキャンしてください。 - jsdomにはポップオーバーがありません。jsdomは閉じた
[popover]を隠すHTMLの規則は備えていますが、showPopover()はなく、:popover-openの読み方も一定しません。DOMにshowPopoverがない場合、エンジンは開いた<dialog>以外のすべての[popover]を閉じていて非表示のものとして扱うため、その中身はチェックされません。ページ自身のCSSで表示し続けているポップオーバーも、jsdomではチェックされません。ブラウザーでは、エンジンは表示されているものを読みます。ポップオーバーの中身をチェックするには、ポップオーバーを開いた状態でブラウザーでページをスキャンしてください。 - jsdomでは、CSSの
contentによるテキストは名前になりません。jsdomは::beforeと::afterのスタイルを計算しないため、生成されたテキストしか持たないボタンやリンク(content: "Save"、content: attr(data-label))は、ブラウザーでは名前を持ちますが、jsdomでは名前がなく、button-name-present、link-name-presentなどの名前のルールで不合格になります。そうしたページはブラウザーでスキャンしてください。 - jsdomはフォントサイズを書かれたとおりに保持します。ブラウザーは計算済みのフォントサイズをpxで報告しますが、jsdomはキーワードや
calc()を書かれたとおりに保持します。エンジンは、キーワードをデフォルトが16pxのときのブラウザーのサイズで読み(xx-largeは32px)、長さと数値のcalc()も読むので、大きな文字はブラウザーと同じように区別されます。親のサイズに依存するlargerとsmallerはjsdomでは解決されません。そうしたテキストは通常のテキストの比率で判定され、メッセージにはサイズが0pxと示されます。ブラウザーはどちらも解決します。 - jsdomは、シャドウルート自身のスタイルシートを無視します。jsdom 29.1.1で確認しています。シャドウルート内の
<style>もそのadoptedStyleSheetsも、中の要素の計算済みスタイルに反映されないため、jsdomではそこで指定した色、サイズ、display: noneやvisibility: hiddenが見えません。ルールはコンポーネントの中身をスタイルがないものとして判定します。コントラストはデフォルトの色で計測され、CSSで隠したコンテンツは表示されているものとして判定されます。唯一の例外はリンクの色で、コントラストのヘルパーがシャドウルートのスタイルシートから自分で求めます。スタイルを付けたWebコンポーネントで作ったページは、ブラウザーでスキャンしてください。 - jsdomで計算される
text-shadowは、同じ要素を2回目に読むと信頼できません。jsdom 29.1.1で確認済みです。同じ要素の計算済みtext-shadowの値を2回目に読むと、どのアクセサーを使っても、その要素について新たに取得したどのCSSStyleDeclarationからでも、キャッシュの有無に関係なく、実際に宣言された値ではなく「影なし」という誤った別の値が黙って返されます。1回目の読み取りは常に正しい値です。このエンジンは内部で、各要素のtext-shadowを1回の実行につきちょうど1度だけ読み、その結果をキャッシュすることでこれを回避しています(src/core/contrast-helpers.jsの__textShadowInfoElを参照)。そのため1回のスキャンには影響しません。問題が表に出るのは、同じjsdomでパースした要素に対して、getComputedStyle(el).textShadowを自分で2回以上読んだ場合だけです。本物のブラウザーにはこのバグはありません。 - 同じjsdomのDOMを2回目にスキャンすると、ショートハンドを上書きするスタイルが失われることがあります。jsdomは要素の計算済みスタイルを、一度読むと次にDOMが変わるまで保持しますが、そのキャッシュから返すコピーでは、詳細度の低いルールのショートハンドを上書きするロングハンドが失われます。
*{background:#888}とstyle="background-image:url(hero.png)"がある場合、1回目の読み取りでは画像が得られ、それ以降はすべてnoneになります。スキャンは最初に読んだスタイルを使い続けるので影響を受けませんが、変更のない同じDOMを2回目にスキャンすると、画像を見落とし、1回目で確認を求めたテキストを合格にすることがあります(contrast-computable)。新しくパースしたものをスキャンするか、スキャンの間にDOMを変更してjsdomのキャッシュを消してください。@surea11y/core/testingは自身の2回目のスキャンでそうしています。本物のブラウザーにはこのバグはありません。 - jsdomでは、スキャン時間は要素数ではなくDOMの深さの2乗に比例して増えます。jsdomは
getComputedStyleを呼ぶたびに要素の祖先をたどって継承されたCSSを解決するので、要素ごとに1回呼ぶとO(要素数×深さ)のコストがかかります。エンジンのコードを一切介さずにjsdom 29.1.1で計測したところ、1本の祖先チェーンに並んだ4000個の要素ではgetComputedStyleだけで8.3秒かかり、同じ4000個を兄弟要素として並べた場合は0.25秒でした。エンジン自身が祖先をたどる処理には上限があり線形のままなので、これはルールではなくjsdomのコストです。影響を受けるのはrunDomRulesInPageと、それを土台にしたすべてのもの(@surea11y/test-matchersを含む)です。本物のブラウザーは継承スタイルをネイティブに計算するので、このような特性はありません。コンポーネントフレームワークでは100〜300階層のネストはよくありますが、その程度なら十分に高速です。目立ってくるのはおよそ1000階層を超えてからです。 - jsdomでは、スキャン時間はスタイルシートの大きさにも比例して増えます。jsdomは、すべてのCSSルールを要素と照合してスタイルを計算します。エンジンはスキャン中にDOMを変更しないので、各要素の計算は1回だけですが、その1回にO(ルール数)のコストがかかります。保存したニュースサイトのページ(要素7,916個、CSSルール2,683個、jsdom 29.1.1)では、243秒のスキャンのうち約224秒が、5,735個の要素の最初の計算にかかりました。これはjsdomのコストで、本物のブラウザーは同じページを1秒もかからずに計算します。jsdomはカスタムプロパティも置換しないため、
var()で指定した色はそこでは読み取れません。そのテキストや背景は計測されず、確認が求められます(contrast-computable)。大規模な本番のページには本物のブラウザーを使ってください。 - jsdomでは、リンクの色をページ自身のルールから求めます。jsdomは自身の
:link { color: #0000EE }をページのルールと詳細度だけで比べるため、ページのa { color: #fff }がそれに負けてしまいますが、ブラウザーはページのルールを適用します。jsdomでは、エンジンはリンクとその中のテキストの色を、ブラウザーのカスケードと同じようにページのルールから求めます。ページのどのルールも色を付けていないリンクはブラウザーのリンク色のままで、リンクは未訪問として扱います。それができない場合(読み取れないスタイルシート、var()、@supports、@layer、@containerの中のルールやネストしたルール)は、jsdomの青で計測せずに、そのテキストについて確認を求めます(contrast-computable、FOREGROUND_UNPARSABLE)。ブラウザーでは、計算済みの色をそのまま読みます。 - jsdomでは、テキストは計算済みの
font-sizeで判定されます。ブラウザーでは、コントラストのルールとp-as-headingは、テキストを描画されるサイズで判定します。CSSのzoom、SVGのtransform、font-size-adjustも含み、最後のものはフォント自身のメトリクスから求めます(font-size: 24px; font-size-adjust: 0.3はArialを約14pxで描画し、これは大きな文字ではありません)。jsdomはレイアウトを行わずフォントも読まないため、そこではfont-sizeをそのまま使います。そのようにテキストを拡大縮小するページは、ブラウザーでスキャンしてください。 - 実際のブラウザーでは、一部のルールが確認の間だけページを変更し、ページ自身のスクリプトがそれに反応することがあります。ブラウザーが描画する内容を計測するため、
css-hidden-focusはキーボードと同じように要素にフォーカスを当て、計測する要素のトランジションを無効にします。aria-hidden-focusは、ページがすぐにフォーカスを移すかどうか(フォーカストラップの番兵)を確かめるため、aria-hiddenのサブツリー内の要素にフォーカスを当てます。text-spacing-content-lossはWCAG 1.4.12の間隔を指定したスタイルシートを追加します。コントラストのルールは、プレースホルダーやドロップダウンの<select>についてブラウザー自身の色を読み取るため、画面外に空のフレームを追加します。スキャンは、ルールの実行中、実行中の各アニメーションを固定した時点に移します(有限のものは終わりに、無限のものは始まりに)。そのため、フェードインは落ち着いた状態で計測されます。これらはすべて、ルールやスキャンが終わる前に元に戻します。フォーカス、スクロール位置、style属性、アニメーション、追加したスタイルシート、フレームです。元に戻せないのは、それに反応してページのスクリプトが行う処理です。要素にフォーカスを当てるとそのfocusとfocusinのハンドラーが実行されるため、カルーセルがフォーカスされたスライドに移動してaria-hiddenも一緒に移したり、ボタンが有効になったりすることがあります。そのスキャンの検出結果は、見つけた時点のページのものです。aria-hidden-focusは、どのサブツリーが非表示か、そして報告するマークアップを、何かにフォーカスを当てる前に読み取ります。ただし、ページはスクリプトが変えたままの状態で残るため、同じページを2回目にスキャンすると別のものが見つかることがあります。引き続き使うページをスキャンする場合や、1つのページを2回スキャンする場合は、その間に再読み込みしてください。 - 別のオリジンのスタイルシートは、ページからは読み取れません。ページのスクリプトは、別のオリジン(CDN、フォントサービス)が配信するスタイルシートのルールを列挙できないため、
css-orientation-lockはそれについて確認を求め(STYLESHEETS_NOT_READABLE)、css-hidden-focusとcss-focus-indicator-suppressedはその中のフォーカスのルールを見られません。そのテキストをengineOptions.styleSheetTextsで渡してください。preloadStyleSheets()はサーバーが共有しているもの(CORS)を取得し、ブラウザーを操作するツールならそれ以外のものもページの外から取得できます(GitHub上のINTEGRATION.mdを参照)。 - 静的なマークアップと、ライブ/ハイドレーション後のDOMの状態。ルールのロジック自体はDOMの出どころを問いません。jsdomでパースした静的なHTML(パターン1)でも、読み込みとハイドレーションが済んだ本物のブラウザーのタブ(パターン2。
INTEGRATION.mdを参照)でも、渡されたDOMを評価します。ただしCLI(npx @surea11y/cli scan <url>)は、JSを実行せずに静的なHTMLだけを取得します(CLIのドキュメントを参照)。JSフレームワークでハイドレーションされるウィジェットで、サーバーレンダリングされたマークアップが、クライアントのJSが同期する前の状態を意図的に出している場合(たとえば<input type="checkbox" aria-checked="true">を出しておき、ハイドレーション時にクライアントのJSがネイティブのcheckedプロパティをそれに合わせる。ごく一般的で、まったく正当なパターンです)、CLIのスキャンにはハイドレーション前のマークアップしか見えません。実際に読み込まれたブラウザーのタブ内で動くスキャンにはハイドレーション後の状態が見えるので、まさにこの種の要素について、ルールの正しさとは無関係な理由で両者の結果が食い違うことがあります。aria-checked-state-mismatchを確定的なfailではなくmanual/cantTellまでに抑えているのはそのためです。ハイドレーションの影響を受けるチェックでライブDOMの正確さが必要なら、静的に取得するCLIではなく、パターン2で読み込み済みのページに対してライブラリを直接実行してください。 - レイアウトは、1つのビューポートで、その時点でページが持っているフォントを使って計測されます。ブラウザーが描画する内容を計測するルール(
text-spacing-content-loss、target-size-minimum、skip-link)の結果は、ページをスキャンしたビューポートでのみ成り立ちます。レスポンシブなページは、ある幅ではテキストを切ったりターゲットを小さくしたりしても、別の幅ではそうしないことがあります。また、Webフォントの読み込み前に計測したテキストは、幅が異なります。結果はその両方をengine.environmentに記録し、これらのルールの検出結果は、ビューポートを繰り返し記録したうえで、何を計測したか(テキストが端からどれだけはみ出したか、ターゲットの大きさと隣のターゲットとの近さ)を示します。そのため、実行によって異なる検出結果を、その条件までたどることができます。必要な幅ごとにスキャンし、スキャンの前にページが落ち着くのを待ってください。waitForPageReady()はloadイベント、Webフォント、画像を待ちます(スキャンの前に呼び出す方法はINTEGRATION.mdを参照してください)。結果には、フォントや画像がまだ読み込み中だったかどうかがengine.environmentに記録されます。 - ブラウザーでは、コントラストは、その時点のページでテキストの下に描画されているものに対して計測されます。画像、グラデーション、テキストの一部だけを覆う描画の上にあるテキストは、計測せずに確認を求めます(
contrast-computable、理由コードBACKGROUND_OVERLAP)。overflow: hiddenのボックスが完全に表示範囲外に置いているテキスト(トラックの外にあるカルーセルのスライドなど)は、表示されるまで対象外になるので、それを確認するにはその状態でページをスキャンしてください。テキストの上にある固定配置やstickyのボックスは無視されます。jsdomでは、テキストは祖先に対してだけ計測されます。強制カラー(Windowsのハイコントラストや、エミュレートしたforced-colors: active)のもとでスキャンした場合、計測の間はページ自身の色に保たれるので、判定結果は強制カラーをオフにしたときと同じになります。ただし、ページ自身のCSSが強制カラーに反応する場合(forced-colorsのメディアクエリや、GrayTextのようなシステムカラー)は別で、スキャンはそれをengine.environment.forcedColorsCssで報告します。ページ自身の色に保てない場合(スタイルシートを採用できないブラウザーで、Content Security Policyがエンジンの<style>をブロックしている場合)は、計測される色の一部がブラウザーの色になり、コントラストの各出現箇所がそのことを示します(details.forcedColors)。計測の仕組みの詳細は、GitHubのLAYOUT_MEASUREMENT.mdにあります。 - 背景のないページのテキストは、キャンバスがわかる場合、ページの配色のキャンバス上で計測されます。不透明な背景を描画しないページの背後には、ブラウザーがキャンバスを描画します。その色は、ページが描画される配色の
Canvasの色で、配色はページ自身のcolor-schemeで決まります。何も宣言しないページでは、ユーザーの好みにかかわらず白、ダークで描画されるページではダークのキャンバスです。コントラストのルールは、どちらのモードでもそこでテキストを計測します。計測せず、contrast-computableが確認を求めるもの(BACKGROUND_NOT_OPAQUE_AT_ROOT)は、周りのページが透けて見えることがあるキャンバスを持つフレーム内のテキストと、キャンバスがユーザーのパレットになる強制カラーのもとでのテキストです。 - jsdomでは、テキストは、求められる場合はページが描画される配色の色で計測され、求められない場合は確認が求められます。jsdomはライトの配色の色を計算し、
prefers-color-schemeのルールを適用しません。ダークで描画されるページ(ルートや<meta name="color-scheme">でcolor-scheme: dark)では、配色が色を付けるテキストはダークの配色の色(Chromiumのもの)になり、light-dark()、CanvasText、LinkText、Canvasで指定したテキストの色はダーク用に求めます。その配色での色を求められないテキストは、ライトの色で計測せずに確認を求めます(contrast-computableのCOLOR_SCHEME_NOT_RENDERED、link-in-text-blockのCOLOR_NOT_COMPUTABLE)。ブラウザーでは、色はブラウザーが描画するものです。 - ライトとダークの両方に対応するページでは、スキャンが見ていない配色は、テキストの色がそれによって変わりえない場合にだけ計測されます。スキャンが見るのはブラウザーが優先する配色です(jsdomではライト)。キャンバス上のテキストはもう一方の配色でも計測しますが、ページが指定した
prefers-color-schemeのルール、light-dark()の値、システムカラーがテキストの色やその背後の背景に及ぶ場合と、スタイルシートを読み取れない場合は除きます。アウトライン、影、::first-line、疑似要素で描かれたテキストや、ほかの描画の上で計測されるテキストも、もう一方の配色では計測しません。data.details.colorSchemesCheckedは計測した配色を列挙します。もう一方の配色を確認するには、ブラウザーをその配色にしてもう一度スキャンしてください。 - 生成されたテキストは、ブラウザーでだけ、しかも読み取れる場合にだけ計測されます。
::beforeや::afterのcontentが書き出すテキストと、リスト項目の::markerは、その疑似要素の色とフォントで計測されます。読み取れない内容(ページ独自の@counter-styleのカウンター)や、独自の背景を持たずにフローの外に配置された::beforeや::afterは、代わりに確認を求めます(contrast-computable)。マーカーは<li>でだけ探します。疑似要素のスタイルを計算しないjsdomでは、生成されたテキストは対象外です。 - フォームフィールドのテキストとプレースホルダーは、フィールドが描画するとおりに計測されますが、計測しないものもあります。日付や時刻のフィールドが空のときにブラウザーが描画するテキストと、ファイル入力のボタンとファイル名は計測しません。ドロップダウンの
<select>は、閉じたコントロールが描画するとおりに、表示中の選択肢をselect自身の色で計測します。独自の色を持つほかの選択肢は、開いたときにプラットフォームが描画するリストにしか表示されないため、cantTellになります(SELECT_OPTION_COLOR)。比率を下回るプレースホルダーは、ページが色を選んだ場合は不合格(PLACEHOLDER_BELOW_THRESHOLD)、色がすべてブラウザー自身のものである場合は比率を満たすもの(達成方法G148)、それがわからない場合はcantTellになります。jsdomでは、プレースホルダーはまったく計測しません。詳細:LAYOUT_MEASUREMENT.md。 target-size-minimumは、ポインターが当たる領域を、ヒットテストではなくレイアウトから求めます。スクロールしないと見えない位置や、スクロールするボックス内にあるターゲットは、そこまでスクロールしたものとして全体を計測します。ターゲットの上にある固定配置やstickyのボックスは無視します。3Dのtransform、ほとんどのclip-pathの形状、祖先による角の丸い切り取りは、バウンディングボックスとして計測します。区切り文字だけを挟んで互いに詰まっているリンク(「Edit | Delete」)は、例外にせず確認を求めます。jsdomでは、このルールはnotApplicableになります。詳細:LAYOUT_MEASUREMENT.md。- コントラストはsRGBで計算されます。
oklch()、oklab()、lab()、lch()、color()で指定された色(color-mix()や相対色の計算値も含みます)は、標準的なディスプレイでの見え方と同じようにsRGBに変換されます。彩度の高いdisplay-p3の赤のようにsRGBの範囲外にある色は、その範囲の端に切り詰められます。これはChromiumがsRGBの画面に描画するのと同じです。広色域の画面では、その色がコントラスト比の前提よりも鮮やかに表示されることがあります。エンジンが変換できない背景色(知らない色空間のcolor())は透明として扱いません。contrast-computableが理由コードBACKGROUND_UNPARSABLEでそのテキストについて確認を求め、ほかのコントラストのルールはそのテキストを対象から外します。
対象外:安全に自動化できない判断
これらには、このエンジンの基準(failは決定論的な違反のためだけに取っておく。例外はありません)を満たすほど安全なヒューリスティックがありません。それでも作れば、ほとんど何も検出しない(狭すぎて役に立たない)か、実際に誤検出を生む(広すぎて信頼できない)かのどちらかになります。
- 「この見出し/ラベルのテキストは意味をなしているか」:実際の見出しやラベルは実にさまざまで、短くても正当なことがよくあります(「FAQ」「名前」「概要」はどれも問題ありません)。そのため、見出しがその下のセクションを説明しているか、ラベルが隣のフィールドを説明しているかを、マークアップから判断することはできません。判断できるのは、何も説明しえない文字列があるということです。
heading-qualityとform-control-label-qualityは、消し忘れたプレースホルダー、番号付きのテンプレート枠、ファイル名、URLを、厳選した完全一致リストに照らして指摘します。link-name-qualityと同じく、見逃しを許容してでも誤検出を避けるという、意図したトレードオフです。どちらもcantTellまでに抑えたmanualルールで、レビューの候補を挙げるだけで、テキストが間違っていると断定することはありません。 - 「このエラーメッセージは問題を説明しているか」:何がバリデーションエラーを発生させるか、その内容が何かは、ほぼ常にJSやバリデーションライブラリが決めるもので、そもそも静的なスキャンからは見えません。ヒューリスティックの設計だけの問題ではありません。
- 時間依存メディアの細かいサブチェック(WCAG 1.2.xには、このエンジンに対応するルールのないACTルールが13あります。
ACT_RULE_MAPPING.mdのギャップの節を参照):音声・映像のコンテンツそのものは、静的なマークアップからは本質的に検証できません。最も広く安全な2つのケース(media-alternative-transcript-evidence、video-caption)はカバーしていますが、より狭いケースは意図的にカバーしていません。 - 文字画像の内容分析(WCAG 1.4.5/1.4.9):画像がテキストを含むかどうかはそのピクセルの中にあり、それにはOCRに相当する画像理解が必要です。
image-of-textが確認を求めるのは、マークアップに手がかりがある場合だけです。<input type="image">と、マークアップに<text>を含むSVGのデータURLを表示する<img>がそれに当たります。文章を撮った写真、テキストを焼き込んだバナー、テキストのCSS背景画像には手がかりがなく、確認も求めません。例外を認めない1.4.9にはルールがありません。 - 動きによる起動の操作(WCAG 2.5.4):ニッチで、実際に問題になる頻度も低いものです。優先していないだけで、構造的に不可能なわけではありません。
img-alt-decorativeの2つのACTエッジケース:現在のネットワークリクエストの状態が「完全に利用可能」でない(読み込み中、または壊れている)<img>と、完全に透明な(実際には何も描画されていない)<canvas>は、どちらもACT自身の適用範囲から除外されています。どちらも静的なDOMスキャンでは判断できない(画像のデコードも、canvasのピクセルの読み戻しもしない)ので、除外せずに対象に残しています。人間のレビュアーなら一目で却下できるまれな誤検出のほうが、本当に除外対象でない/装飾でない要素を黙って報告し損ねるより安全だからです。
実際に意味すること
ここに挙げたことの多くは、surea11yに固有のものではありません。DOMの1つの状態を判定するツールはどれも1回のスキャンで見えないものの制限を共有しており、ほとんどの自動ツールは判断を要するものも人に任せています。ただし、ツールによって、実行のしかた、チェックする内容、判断できないものの報告のしかたは異なります。それでもここで明記する理由は、このエンジン(あるいはどんな自動ツール)のpassも、上に挙げた達成基準についてWCAG自体が求めている手動レビューの代わりには決してならないからです。ルールのpassが何を主張し、何を主張しないか、そしてルールがすべてpassでもWCAGの達成基準がcantTellのままなのはなぜかについてはWCAG_CONFORMANCE.mdを参照してください。