CSSで、キーボード操作のときのフォーカス枠を見やすくする方法

​​CSS​HTML,Playwright,アクセシビリティ

キーボードで操作する人は、フォーカスの枠を見て、いまどこを選んでいるかを知ります。:focus-visible に outline と outline-offset を書くと、Tab キーで選んだときだけ太くはっきりした枠が出て、マウスでボタンを押したときには出ません。暗い背景にも置く部品なら、濃い色と白の2色の枠にすると、どちらの背景でも見えます。

補足

検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。HTML と CSS のページを Playwright で開き、Playwright からキーボードの Tab キーとマウスのクリックを送ってフォーカスを動かし、getComputedStyle() の値とスクリーンショットの画素を記録しました。強制カラーモードは Playwright の emulateMedia({ forcedColors: 'active' }) で再現したもので、Windows のハイコントラスト設定の実機ではありません。Firefox と Safari、スマートフォンの実機は試していません。

3つのボタン「保存」「複製」「削除」が横に並んだ画面。Tab キーを押すたびに、水色の太い枠が左のボタンから順に移り、上の帯に「Tab 1回目/3回」などと回数が出る。最後にマウスで「保存」をクリックすると、カーソルの位置にピンクの輪が広がり、フォーカスは「保存」に移るが枠は出ず、「:focus-visible なし(枠なし)」と表示される
Tab キーを3回押したあと、マウスで「保存」をクリックする動き。Tab キーで選んだときだけ枠が出る

:focus-visible に outline を書く

枠は次の CSS で出します。太さ 3px の線を、ボタンの縁から 2px 離して描きます。

css
.btn:focus-visible {
  outline: 3px solid #067a98;
  outline-offset: 2px;
}

検証環境でページを開き、Tab キーを押してボタンを順に選ぶと、どのボタンでも :focus と :focus-visible の両方が当てはまり、枠が出ました。マウスでクリックした場合は、フォーカスはボタンに移って :focus は当てはまるものの、:focus-visible は当てはまらず、枠は出ませんでした。上の GIF の最後の場面がこの状態です。

:focus に同じ枠を書いたボタンと比べると、違いはクリックしたときだけでした。

書き方 Tab キーで選ぶ マウスでクリック
.btn-focus:focus に枠 枠が出る 枠が出る
.btn:focus-visible に枠 枠が出る 枠が出ない
テキストの入力欄に :focus-visible で枠 枠が出る 枠が出る

テキストの入力欄だけは、クリックしても :focus-visible が当てはまりました。MDN の :focus-visible のページにも、ボタンをクリックしたときは一般にフォーカスを表示しないが、文字を入力する欄では表示する、とあります。:focus-visible が当てはまるかどうかはブラウザが決めるので、CSS で決められるのは、当てはまったときの見た目だけです。

何も書いていないボタンでも、Tab キーで選ぶと Chromium の既定の枠が出ました。そのときの値は outline: auto 1px rgb(16, 16, 16) で、細い枠です。上の CSS は、この既定の枠を、太くて色のはっきりした枠に置き換えています。

MDN のブラウザー互換性データ(@mdn/browser-compat-data 8.1.8)によると、:focus-visible は Chrome と Edge が 86、Firefox が 85、Safari が 15.4 から使えます。

枠は border ではなく outline で描く

フォーカスしたときに border を太くして枠を出すと、ボタンが大きくなり、まわりの要素が動きます。検証環境で、ふだんは 1px の border を、フォーカスしたときだけ 3px にしたボタンを Tab キーで選ぶと、ボタンは 89×46px から 93×50px に大きくなり、右どなりのボタンが 4px 右へずれました。outline で枠を出したボタンは 91×46px のままで、右どなりのボタンも動きませんでした。

CSS の仕様(CSS UI 4)で outline を定めた節にも、outline は場所を取らない(まわりの配置に影響しない)と書かれています。同じ節には、outline は border-radius の丸みに沿って描くとあり、検証環境でも、角を丸めたボタンの枠は丸い角に沿っていました。

outline-offset は、要素の縁と枠のあいだのすき間を指定するプロパティです。この例では 2px あけて、ボタンの灰色の縁と枠を離しています。WCAG のフォーカスの外観の解説でも、枠を部品から少し離すと、枠が見やすくなることがあると書かれています。

暗い背景でも見えるよう、2色の枠にする

1色の枠は、背景の色によって見えにくくなります。濃い水色(#067a98)の枠を、白い背景と暗い背景(#1f2a30)に置いて比べました。

WCAG 2.2 の達成基準 2.4.13(フォーカスの外観、レベル AAA)は、フォーカスの枠に2つのことを求めています。1つは、フォーカスしていない部品を太さ 2px の線で囲んだときの面積以上の大きさがあること。もう1つは、その部分の画素が、フォーカスしていないときの同じ画素と比べて 3:1 以上のコントラスト比で変わることです。

そこで、フォーカスする前と後のスクリーンショットで、ボタンの左の外側(縦の中央)の同じ画素の色を取り、WCAG の相対輝度とコントラスト比の式で計算しました。

枠 白い背景でのコントラスト比 暗い背景でのコントラスト比
1色(#067a98) 4.95 2.96
2色(#1f2a30 と白) 14.66 14.66

1色の枠は、暗い背景では 2.96 で、3:1 に届きませんでした。2色の枠は次のように書きます。

css
.btn-2tone:focus-visible {
  outline: 3px solid #1f2a30;
  outline-offset: 0;
  box-shadow: 0 0 0 6px #fff;
}

内側に濃い色の outline を 3px 描き、その外側に、白い box-shadow を出します。box-shadow はボタンの縁から 6px 広がります。検証環境の Chromium では、内側の 3px に outline が重なって描かれ、ボタンの縁から外へ 1〜3px が濃い色、4〜6px が白でした。白い背景では濃い色の線が、暗い背景では白い線が、それぞれ 3px の太さで見えます。

白い背景と暗い背景のそれぞれに、1色の枠と2色の枠のボタンを並べた画面。白い背景では、どちらの枠もはっきり見える。暗い背景では、1色の濃い水色の枠は背景になじんで見えにくく、2色の枠は外側の白い線がはっきり見える
Tab キーで選んだボタンの枠。数字は、フォーカスする前と後の同じ画素のコントラスト比

box-shadow で描くときは、透明な outline を残す

強制カラーモードは、Windows のハイコントラスト設定のように、ページの色を利用者が選んだ色に置き換える表示です。outline: none にして box-shadow だけで枠を描く書き方では、強制カラーモードで枠が消えました。

CSS の仕様で強制カラーモードの影響を定めた節には、強制カラーモードのとき box-shadow は none になり、outline-color などの色はシステムカラーに置き換わる、と書かれています。そこで、box-shadow で描くときも、透明な outline を残しておきます。

css
.btn-shadow-safe:focus-visible {
  outline: 3px solid transparent;
  box-shadow: 0 0 0 3px #067a98;
}

ふだんの表示では、透明な outline は見えず、box-shadow の枠だけが見えます。強制カラーモードを再現して Tab キーでボタンを選ぶと、box-shadow は none になりました。outline: none のボタンは、ボタンのまわり 6px の範囲で色の変わった画素が 0 個で、フォーカスしても見た目が変わりませんでした。透明な outline を残したボタンは、outline の色が rgba(0, 0, 0, 0)(透明)から Chromium の決めたシステムカラーに置き換わり、枠が見えました。

ふだんの表示と強制カラーモードで、box-shadow だけで枠を描いたボタンと、透明な outline も書いたボタンを比べた画面。ふだんはどちらも水色の枠が出ている。強制カラーモードでは、box-shadow だけのボタンは枠がなく、透明な outline を書いたボタンには濃い紫の枠が出ている
Tab キーで選んだボタン。下の段は強制カラーモードを再現した表示

上の2色の枠も、内側の線は outline で描いています。強制カラーモードを再現して Tab キーで選ぶと、白い box-shadow は none になり、outline の線はシステムカラーで残りました。

overflow: hidden の箱の中では、枠を内側に描く

overflow: hidden の箱の中で、箱の端いっぱいに広がったリンクに外側の枠を描くと、箱からはみ出た部分が切れます。角を丸めたメニューの1行目のリンクに、outline-offset: 2px の枠を出して画素を数えると、枠の色の画素は上の辺と左右の辺で 0 個で、残っていたのは下の辺だけでした。

MDN の outline-offset のページにあるとおり、outline-offset に負の値を書くと、枠は要素の内側に描かれます。

css
.menu {
  overflow: hidden;
}
.menu a:focus-visible {
  outline: 3px solid #067a98;
  outline-offset: -3px;
}

outline-offset: -3px にすると、太さ 3px の枠がリンクの内側にちょうど収まり、上下左右の4辺とも枠の色の画素がありました。

角を丸めたメニューの1行目「ホーム」に枠を出した画面を、上下に2つ並べたもの。上は outline-offset: 2px で、枠の上と左右が切れ、「ホーム」と「記事一覧」の間に水色の線が1本あるだけに見える。下は outline-offset: -3px で、「ホーム」の行を水色の枠が4辺とも囲んでいる
overflow: hidden の箱の中のリンクを Tab キーで選んだ画面。上は外側に、下は内側に枠を描いた

​Posted by 86