Gemcook Tech Blog
🏷️

モダンCSSでWebの課題をスマートに解決!一歩進んだアクセシビリティ対応テクニック3選

に公開

前回の記事では:focus-visibleによるフォーカスインジケーターのカスタマイズ、カラーコントラスト、visually-hiddenパターンについて紹介しました。
今回はその続編として、CSSを用いたアクセシビリティ対応のポイントをさらに3つ紹介します。

https://zenn.dev/gemcook/articles/css-accessibility-tips2

prefers-contrastを使ったハイコントラストモード対応

みなさんはOSの「コントラストを上げる」設定を使ったことはありますか?
前回の記事ではCSSで色を定義する際にコントラスト比を確保する方法を紹介しましたが、今回はユーザーがOS側でコントラストの設定を変更した場合に、CSSでその設定を検知してスタイルを調整する方法を紹介します。

視力の弱いユーザーや色覚特性のあるユーザーの中には、OS側でハイコントラストモードを有効にしている方がいます。
prefers-contrastメディア特性を利用すると、その設定を検知してWebサイト側でもコントラストを強化することができます。

詳しくは以下の「Web Content Accessibility Guidelines (WCAG)」を参照してください。

https://waic.jp/translations/WCAG21/Understanding/contrast-enhanced.html

prefers-contrastのメディア特性について

prefers-contrastには以下の値があります。

説明
no-preference ユーザーが特に設定を行っていない状態(デフォルト)
more より高いコントラストを希望している状態
less より低いコントラストを希望している状態
custom 特定のカラーパレットを指定している状態(forced-colors: activeと関連)

https://developer.mozilla.org/ja/docs/Web/CSS/@media/prefers-contrast

対応方法について

prefers-contrast: moreを利用して、ハイコントラストモード時にスタイルを調整するコードは以下になります。

:root {
  --color-text: #555555;
  --color-bg: #ffffff;
  --color-border: #dddddd;
  --color-link: #1a73e8;
}

/* ハイコントラストモード時のスタイル調整 */
@media (prefers-contrast: more) {
  :root {
    --color-text: #000000;
    --color-bg: #ffffff;
    --color-border: #000000;
    --color-link: #0000ee;
  }

  /* プレースホルダーのコントラストを強化 */
  ::placeholder {
    color: rgba(0, 0, 0, 0.8);
    opacity: 1;
  }

  /* 無効化された要素のコントラストを強化 */
  [disabled] {
    color: rgba(0, 0, 0, 0.7);
  }

  /* 選択テキストのtext-shadowを無効化して視認性を向上 */
  ::selection {
    text-shadow: none;
  }

  /* イタリックや小さいテキストの太さを強化 */
  em, i, small {
    font-weight: bold;
  }
}

ポイントとしては以下になります。

  • CSS変数を利用してカラーを管理することで、prefers-contrast: moreの中でまとめて色の上書きを行うことができる。
  • ::placeholder[disabled]など、デフォルトでコントラストが低くなりがちな要素も忘れずに対応する。
  • ::selectiontext-shadowを無効にすることで、テキスト選択時の視認性を向上させる。
  • イタリックや小さいテキストは、ハイコントラストモードでは読みにくくなりやすいためfont-weight: boldを追加して可読性を高める。

forced-colorsとの違いについて

prefers-contrastと似たメディア特性としてforced-colorsがあります。
混同しやすいポイントなので、違いを簡単に整理します。

メディア特性 目的
prefers-contrast ユーザーがコントラストの強弱を希望しているかを検知する。Webサイト側でスタイルを調整する。
forced-colors OS側で色が強制的に上書きされている状態を検知する(Windowsのハイコントラストモードなど)。

forced-colors: activeの状態では、ブラウザが自動的に色をシステムカラーに置き換えるため、CSSで設定した色が無視されることがあります。
そのため、forced-colors環境下ではCSSシステムカラー(CanvasCanvasTextButtonFaceButtonTextなど)を利用してスタイルを定義する必要があります。

@media (forced-colors: active) {
  .card {
    border: 1px solid ButtonText;
  }

  .button {
    background-color: ButtonFace;
    color: ButtonText;
    border: 1px solid ButtonText;
  }
}

https://developer.mozilla.org/en-US/docs/Web/CSS/@media/forced-colors

ハイコントラストモードの検証を行いたい時に

Chrome DevToolsを利用してprefers-contrastの検証を行うことができます。

  1. DevToolsを開く(F12 or Cmd + Option + I
  2. 右上の「︙」→「More tools」→「Rendering」を選択
  3. 下にスクロールして「Emulate CSS media feature prefers-contrast」を見つける
  4. morelesscustomから選択する

Chrome DevToolsでの確認手順1

Chrome DevToolsでの確認手順2

Chrome DevToolsでの確認手順3

これにより、OSの設定を変更せずにハイコントラストモードの表示確認を行うことができます。

Mac / iOSで設定する

Macの場合

  1. メニュー → システム設定 を開く
  2. 左サイドバーで アクセシビリティ を選択
  3. 右ペインの ディスプレイ をクリック
  4. 「コントラストを上げる」 をオンにする

Macでの確認手順1

Macでの確認手順2

iOSの場合

  1. 設定アプリを開く
  2. アクセシビリティ をタップ
  3. 画面表示とテキストサイズ をタップ
  4. 「コントラストを上げる」 をオンにする

iPhoneでの確認手順1

iPhoneでの確認手順2

iPhoneでの確認手順3

scroll-margin-topを使った固定ヘッダーでのアンカーリンク対応

固定ヘッダー(sticky header)のあるWebサイトで、アンカーリンクをクリックした時にコンテンツがヘッダーの下に隠れてしまった経験はありませんか?

これは、ブラウザがアンカーリンクのターゲット要素をビューポートの最上部にスクロールするために起こります。
固定ヘッダーがある場合、ターゲット要素の先頭部分がヘッダーの裏に隠れてしまい、ユーザーはコンテンツの先頭を見ることができません。

この問題は、特にキーボードナビゲーションやスクリーンリーダーを利用しているユーザーにとって、現在どの位置にいるかがわかりにくくなるため、アクセシビリティ上の問題になります。

従来の対応方法の問題点

従来はアンカーターゲットにpadding-topmargin-top(負の値)を組み合わせる方法が使われていました。

/* ❌ 従来の方法:レイアウトに影響が出やすい */
.anchor-target {
  padding-top: 80px;
  margin-top: -80px;
}

この方法ではレイアウトに意図しない影響が出たり、空のHTML要素を追加する必要があったりと、保守性に問題がありました。

scroll-margin-topを利用した対応方法

scroll-margin-topを利用すると、レイアウトに影響を与えずにスクロール位置のオフセットを設定することができます。

<header class="header">ナビゲーション</header>

<main>
  <section id="section-1">
    <h2>セクション1</h2>
    <p>コンテンツ...</p>
  </section>
  <section id="section-2">
    <h2>セクション2</h2>
    <p>コンテンツ...</p>
  </section>
</main>
.header {
  position: sticky;
  top: 0;
  height: 60px;
  background: #fff;
  z-index: 100;
}

/* id属性を持つすべての要素にscroll-margin-topを設定 */
[id] {
  scroll-margin-top: 80px; /* ヘッダーの高さ + 余白 */
}

https://developer.mozilla.org/ja/docs/Web/CSS/scroll-margin-top

ポイントとしては以下になります。

  • [id]セレクタを利用することで、id属性を持つすべての要素に一括で適用できる。
  • scroll-margin-topの値はヘッダーの高さよりも少し大きめに設定すると、余白ができてコンテンツが見やすくなる。
  • scroll-margin-topはスクロール時のみ影響し、通常のレイアウトには影響を与えない。

scroll-padding-topとの使い分け

似たプロパティとしてscroll-padding-topがあります。
違いは以下の通りです。

プロパティ 適用先 用途
scroll-margin-top スクロール先の子要素に設定 個別のアンカーターゲットごとにオフセットを調整したい場合
scroll-padding-top スクロールコンテナ(親要素)に設定 コンテナ内のすべてのスクロール先に一括でオフセットを設定したい場合

固定ヘッダーの対応としては、html要素にscroll-padding-topを設定する方法もシンプルで便利です。

html {
  scroll-padding-top: 80px; /* ヘッダーの高さ + 余白 */
}

レスポンシブ対応について

モバイルとデスクトップでヘッダーの高さが異なる場合は、CSS変数とメディアクエリを組み合わせて対応しましょう。

:root {
  --header-height: 56px;
}

@media (min-width: 768px) {
  :root {
    --header-height: 72px;
  }
}

html {
  scroll-padding-top: calc(var(--header-height) + 16px);
}

CSSのorderプロパティとアクセシビリティの注意点

FlexboxやCSS Gridを利用する際に、orderプロパティやflex-direction: row-reverseなどで視覚的な要素の順序を変更したことはありませんか?

これらのプロパティは見た目上の並び順を変更するだけで、DOM(HTMLの構造)上の順序は変わりません
そのため、スクリーンリーダーの読み上げ順序やキーボードのタブ移動順序は、HTMLの記述順のままになります。

この「視覚的な順序」と「DOM上の順序」のずれは、キーボードユーザーやスクリーンリーダーユーザーにとって混乱の原因になり、WCAG 1.3.2「意味のある順序」および WCAG 2.4.3「フォーカス順序」に違反する可能性があります。

https://waic.jp/translations/WCAG21/Understanding/meaningful-sequence.html

https://waic.jp/translations/WCAG21/Understanding/focus-order.html

問題が起こる具体例

以下の例では、orderプロパティを使ってFlexアイテムの表示順を変更しています。

<div class="card-list">
  <a class="card" href="/news" style="order: 3">ニュース</a>
  <a class="card" href="/blog" style="order: 1">ブログ</a>
  <a class="card" href="/about" style="order: 2">会社概要</a>
</div>
.card-list {
  display: flex;
}

視覚的な表示順序: ブログ → 会社概要 → ニュース
キーボードのタブ移動順序: ニュース → ブログ → 会社概要(HTML記述順)

この場合、Tabキーで操作するとフォーカスが画面上で飛び回るように見え、ユーザーは「次にどこにフォーカスが移動するか」を予測することができなくなります。

同様に注意が必要なCSSプロパティ

order以外にも、視覚的な順序とDOM順序にずれを生じさせるCSSプロパティがあります。

プロパティ ずれが起こるケース
order Flex / Grid アイテムの表示順を変更した場合
flex-direction: row-reverse / column-reverse Flexアイテムの並び順が逆になる場合
grid-row / grid-column Grid アイテムを明示的に配置した場合
position: absolute / fixed 要素を通常のフローから外して配置した場合

対応方法について

基本的な方針は「視覚的な順序に合わせてHTMLの記述順を変更する」ことです。

<!-- ❌ orderで表示順を変えている -->
<div class="card-list">
  <a class="card" href="/news" style="order: 3">ニュース</a>
  <a class="card" href="/blog" style="order: 1">ブログ</a>
  <a class="card" href="/about" style="order: 2">会社概要</a>
</div>

<!-- ✅ HTML自体の順序を視覚的な順序に合わせる -->
<div class="card-list">
  <a class="card" href="/blog">ブログ</a>
  <a class="card" href="/about">会社概要</a>
  <a class="card" href="/news">ニュース</a>
</div>

やむを得ずorderを使用する場合は、以下の点を確認しましょう。

  • Tabキーでフォーカスを移動させて、フォーカス順序が視覚的な順序と一致しているかを確認する。
  • スクリーンリーダーで読み上げ順序が自然かを確認する。
  • レスポンシブ対応でモバイルとデスクトップで順序が変わる場合、両方のレイアウトでフォーカス順序を検証する。

Chrome 137以降での新しい解決策: reading-flow

Chrome 137以降では、reading-flowという新しいCSSプロパティが利用できるようになりました。
これにより、視覚的な順序とフォーカス順序を一致させることがCSSだけで可能になります。

.card-list {
  display: flex;
  reading-flow: flex-visual; /* 視覚的な順序に合わせてフォーカス順序を変更 */
}

https://developer.chrome.com/blog/reading-flow

フォーカス順序の検証を行いたい時に

Tabキーでページ内を移動して確認する方法が最もシンプルですが、以下のツールを利用するとより効率的に確認できます。

Accessibility Insights for Web

Microsoftが提供しているChrome拡張機能で、タブ移動時のフォーカス順序を数字付きのオーバーレイで可視化してくれます。

https://accessibilityinsights.io/docs/web/overview/

CSSの工夫でさらにアクセシブルなWebを

今回紹介した3つのテクニックをまとめると以下になります。

  • prefers-contrast: ユーザーのハイコントラスト設定を検知して、CSSでコントラストを強化する。
  • scroll-margin-top: 固定ヘッダーがあるページでアンカーリンクのスクロール位置を適切に調整する。
  • orderプロパティの注意点: 視覚的な順序とDOM順序のずれに注意し、キーボードやスクリーンリーダーの操作性を損なわないようにする。

第1弾で紹介したクリッカブルエリアの調整やprefers-reduced-motion第2弾で紹介した:focus-visibleやカラーコントラスト、visually-hiddenと合わせて、CSSでできるアクセシビリティ対応を意識していきましょう。
どれも小さな改修ですが、ユーザーにとっての体験が大きく変わる可能性があるので、ぜひ少しずつ取り入れてみてください。

今回紹介したテクニックの動作を確認できるデモページを用意しました。ぜひ実際に試してみてください。

https://css-a11y-test-3.netlify.app/

最後までお読みいただき、ありがとうございました!!

Gemcook Tech Blog
Gemcook Tech Blog

Discussion