ボタンをクリックしたあとに青い枠が残るのが気になって、outline: none; で消したことはないでしょうか。見た目はすっきりしますが、これはキーボードだけでサイトを操作している人にとって、今どこを触っているのかが分からなくなるという致命的な変更になります。
この「マウス操作では出したくないが、キーボード操作では必要」という要望に応えるのが :focus-visible です。あわせて、子要素のフォーカスを親側で受け取れる :focus-within を使うと、フォーム全体を強調したりドロップダウンを開いたままにしたりできます。
この記事では、:focus との違いを整理したうえで、フォーカスリングを消さずに見た目を整える書き方と、:focus-visible が効かないときの原因を解説します。
目次
outline: none だけで消してはいけない理由
ブラウザーがフォーカス中の要素に表示する枠線は「フォーカスリング」と呼ばれます。マウスを使っていると、自分がクリックした場所は分かっているので邪魔に感じます。しかし Tab キーでページを渡り歩いている人にとって、フォーカスリングはカーソルそのものです。これを消すのは、テキストエディターから入力カーソルを消すのと同じことになります。
/* やってはいけない書き方 */
button:focus,
a:focus {
outline: none; /* キーボード操作の人が現在地を失う */
}
正しい手順は「消す」ではなく「置き換える」です。既定のリングが気に入らないなら、自分でデザインしたリングを代わりに表示します。そして、そのリングを出す条件を絞り込むために :focus-visible を使います。
:focus と :focus-visible と :focus-within の違い
名前が似ている3つの擬似クラスですが、役割ははっきり分かれています。
| セレクター | マッチする条件 | 主な使いどころ |
|---|---|---|
:focus | フォーカスを持っている要素そのもの。操作方法を問わず、フォーカスがあれば常にマッチする | 操作方法に関係なく見た目を変えたいとき(入力欄の枠線の色など) |
:focus-visible | :focus のうち、ブラウザーが「フォーカス表示が必要」と判断したものだけにマッチする | フォーカスリングのデザイン |
:focus-within | 自分自身、または子孫のどれかがフォーカスを持っている要素にマッチする | フォームやカードなど、親側をまとめて強調したいとき |
言い換えると、:focus-visible は「いつ出すか」を絞り込むもの、:focus-within は「どこを変えるか」を広げるものです。目的が違うので、両方を同じスタイルシートの中で併用することになります。
「キーボードのときだけ」とは限らない
:focus-visible は「キーボード操作のときだけマッチする」と説明されることがありますが、これは正確ではありません。仕様では、どんなときにマッチさせるかの判定はブラウザーに任されています。ブラウザーは直前の操作方法や要素の種類を見て、フォーカス位置を目立たせるべきかどうかを決めます。
実際の挙動として分かりやすいのは、テキスト入力欄です。<input type="text"> や <textarea> はマウスでクリックしただけでも :focus-visible にマッチします。文字を打ち込む場所なので、どこに入力されるかが見えていないと困るからです。一方で <button> をクリックした場合は、多くのブラウザーでマッチしません。
この判定基準はブラウザーの実装によって変わりうるものなので、「クリックでは絶対に出ない」と決めつけた設計にはしないでください。あくまでブラウザーが必要だと考えたときに出る、という理解が正確です。
フォーカスリングをデザインする
基本の型は次の2行です。:focus で既定のリングを消し、:focus-visible で自前のリングを出します。
/* 1. 既定のリングを消す */
.btn:focus {
outline: none;
}
/* 2. 必要なときだけ、自分のリングを出す */
.btn:focus-visible {
outline: 3px solid #1a73e8;
outline-offset: 3px; /* 要素から少し離して見やすくする */
}
outline は要素の大きさに影響しないので、リングを出しても周りのレイアウトがずれません。角丸のボタンでも、モダンブラウザーは border-radius に沿ってリングを描いてくれます。outline-offset は要素からの距離で、少し離したほうが背景と同化せずに見やすくなります。
下のデモで実際の違いを確認できます。プレビューの中をクリックしてから Tab キーを押すと、フォーカスが順番に移動します。左のボタンはリングを消してあるので、フォーカスが当たっても何も起きません。真ん中のボタンは Tab のときだけリングが出て、クリックでは出ません。右の入力欄は、クリックしてもリングが出ることを確かめてみてください。
<div class="demo">
<p class="demo__note">Tab キーで順番に移動してみてください。</p>
<button class="btn btn--none" type="button">リングを消した例</button>
<button class="btn btn--visible" type="button">:focus-visible</button>
<input class="field" type="text" placeholder="入力欄">
</div>
.demo {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
padding: 28px 24px;
font-family: system-ui, sans-serif;
}
.demo__note {
width: 100%;
margin: 0;
color: #555;
font-size: 13px;
}
.btn {
padding: 10px 18px;
border: 1px solid #1a73e8;
border-radius: 6px;
background: #fff;
color: #1a73e8;
font-size: 15px;
cursor: pointer;
}
/* 悪い例: リングを消しただけ。Tab で来ても現在地が分からない */
.btn--none:focus {
outline: none;
}
/* 良い例: 既定のリングは消すが、必要なときだけ自前のリングを出す */
.btn--visible:focus {
outline: none;
}
.btn--visible:focus-visible {
outline: 3px solid #1a73e8;
outline-offset: 3px;
}
.field {
padding: 9px 12px;
border: 1px solid #bbb;
border-radius: 6px;
font-size: 15px;
}
/* 入力欄はマウスでクリックしたときも :focus-visible にマッチする */
.field:focus {
outline: none;
}
.field:focus-visible {
outline: 3px solid #1a73e8;
outline-offset: 2px;
}
色だけで伝えるのは避けてください。色覚の特性によっては、青いリングと灰色の枠線の差が分かりにくいことがあります。太さ・オフセット・背景色の変化など、色以外の手がかりも一緒に変えておくと安全です。
リンクやカスタム要素にも同じ指定をまとめる
ボタンだけに書くと、リンクや入力欄で指定が抜けてしまいます。サイト全体で同じリングを使うなら、フォーカスできる要素をまとめて指定してしまうのが確実です。
/* 共通のリングを変数にしておくと管理しやすい */
:root {
--focus-ring: 3px solid #1a73e8;
}
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
[tabindex]:focus-visible {
outline: var(--focus-ring);
outline-offset: 2px;
}
最後の [tabindex] は、tabindex 属性を付けてフォーカスできるようにした div などを拾うためのものです。既定のリングを消す側も、同じセレクターの一覧を :focus で書いておきます。
:focus-within で親要素ごと強調する
:focus-within は、自分の中のどこかにフォーカスがあるときにマッチします。入力欄そのものではなく、それを囲んでいるカードやフォームに反応させられるので、「今このフォームを入力中です」という状態を面で伝えられます。
次のデモでは、カード全体の枠線と影、そしてラベルの文字色を変えています。入力欄をクリックするか、Tab キーで移動してみてください。
<form class="card">
<p class="card__title">お問い合わせ</p>
<label class="field">
<span class="field__label">お名前</span>
<input class="field__input" type="text" placeholder="山田 太郎">
</label>
<label class="field">
<span class="field__label">メールアドレス</span>
<input class="field__input" type="email" placeholder="mail@example.com">
</label>
<p class="card__hint">入力欄にフォーカスすると、カードとラベルの色が変わります。</p>
</form>
.card {
max-width: 340px;
margin: 24px auto;
padding: 20px;
border: 1px solid #ddd;
border-radius: 10px;
background: #fff;
font-family: system-ui, sans-serif;
transition: border-color 0.2s, box-shadow 0.2s;
}
/* 中のどれかにフォーカスが入ると、カード全体が反応する */
.card:focus-within {
border-color: #1a73e8;
box-shadow: 0 0 0 4px rgba(26, 115, 232, 0.15);
}
.card__title {
margin: 0 0 16px;
font-size: 16px;
font-weight: bold;
}
.card__hint {
margin: 16px 0 0;
color: #777;
font-size: 12px;
}
.field {
display: block;
margin-bottom: 14px;
}
.field__label {
display: block;
margin-bottom: 4px;
color: #555;
font-size: 13px;
transition: color 0.2s;
}
/* label 自身も子孫のフォーカスを受け取れる */
.field:focus-within .field__label {
color: #1a73e8;
font-weight: bold;
}
.field__input {
width: 100%;
padding: 9px 12px;
border: 1px solid #ccc;
border-radius: 6px;
font-size: 15px;
box-sizing: border-box;
}
.field__input:focus {
outline: none;
}
.field__input:focus-visible {
border-color: #1a73e8;
outline: 2px solid #1a73e8;
outline-offset: 1px;
}
ポイントは、.card:focus-within と .field:focus-within のように入れ子で使えることです。外側のカードは「フォームのどこかを入力中」、内側の label は「この項目を入力中」を表し、それぞれ別のスタイルを当てられます。
なお :focus-within には「必要なときだけ」版の擬似クラスがありません。キーボード操作のときだけ親を強調したい場合は、:has() と組み合わせて .card:has(:focus-visible) と書きます。
ドロップダウンメニューを開いたままにする
ホバーで開くメニューは、マウスがないと開けません。:hover と並べて :focus-within を書いておくと、Tab キーで親のリンクにたどり着いた時点でメニューが開き、そのまま中の項目へ移動できます。
.menu {
position: relative;
}
.menu__list {
position: absolute;
top: 100%;
left: 0;
/* display: none は使わない(中の項目にフォーカスできなくなる) */
visibility: hidden;
opacity: 0;
transition: opacity 0.2s;
}
/* マウスでもキーボードでも開く */
.menu:hover .menu__list,
.menu:focus-within .menu__list {
visibility: visible;
opacity: 1;
}
閉じている状態に display: none や visibility: hidden を使っている点に注意してください。これらが指定された要素はフォーカスの対象から外れます。つまり、メニュー項目自体にフォーカスが当たってから開く、という順番は成立しません。先に親メニューの見出しリンクにフォーカスが入り、そこで :focus-within が成立してメニューが表示され、表示されたことで中の項目がフォーカス可能になる、という流れになります。.menu の中にフォーカスできる要素(見出しのリンクやボタン)が必要なのはこのためです。
:focus-visible が効かないときに見るところ
書いたはずのリングが出ない、あるいは消したはずのリングが出続ける。原因はだいたい次の4つのどれかです。
:focus の指定があとに書かれて上書きしている
もっとも多い原因です。:focus と :focus-visible はどちらも詳細度が同じ(擬似クラス1つ分)なので、同じ要素に両方が当たるとあとに書いたほうが勝ちます。リセット CSS の outline: none がファイルの後ろのほうに読み込まれていると、先に書いた :focus-visible のリングが消えてしまいます。
/* NG: あとから :focus で打ち消している */
.btn:focus-visible {
outline: 3px solid #1a73e8;
}
.btn:focus {
outline: none; /* こちらが勝つ */
}
/* OK: 消す指定を先に、出す指定をあとに書く */
.btn:focus {
outline: none;
}
.btn:focus-visible {
outline: 3px solid #1a73e8;
}
読み込み順が分からないときは、開発者ツールで対象の要素を選び、Styles パネルで outline の行に取り消し線が入っていないかを確認すると、どの指定に負けているかがすぐ分かります。
* セレクターで全要素の outline を消している
古いリセット CSS には *:focus { outline: none; } のような一括指定が入っていることがあります。全称セレクター * の詳細度は 0 なので、.btn:focus-visible のようなクラス付きの指定なら勝てますが、a:focus-visible のような要素セレクターだけの指定では読み込み順によって負けます。テーマやフレームワークを使っている場合は、まず outline を一括で消している箇所がないか検索してみてください。
そもそもフォーカスできない要素に指定している
div や span は、標準ではフォーカスを受け取れません。:focus-visible どころか :focus も成立しないので、何も起きないのは当然ということになります。クリックできる見た目の div を作っている場合は、まず <button> に置き換えられないかを検討してください。
どうしても div のままにする必要があるなら、tabindex="0" を付けて Tab キーの移動対象に加えます。ただし tabindex="-1" は「JavaScript から focus() できるが Tab では止まらない」という指定なので、キーボードで到達させたい要素に付けても意味がありません。
<!-- Tab で止まるようになる -->
<div class="custom-btn" tabindex="0" role="button">送信</div>
マウスで操作していてマッチしていないだけ
ボタンをクリックしてリングが出ないのは、多くの場合バグではなく :focus-visible が意図どおりに働いている状態です。確認するときは、プレビュー内をいったんクリックしてから Tab キーで移動して試してください。開発者ツールの Styles パネルにある「:hov」から :focus-visible を強制的に有効にすると、キーボードを使わずに見た目だけを確認できます。
まとめ
フォーカスリングは、キーボードで操作する人にとってのカーソルです。outline: none で消すのではなく、:focus で既定のリングを消したうえで :focus-visible に自分のリングを書く、という2段構えが基本になります。:focus-visible がマッチするかどうかはブラウザーの判断なので、テキスト入力欄のようにクリックでもマッチするケースがあることは覚えておいてください。
:focus-within は、子孫のフォーカスを親で受け取れる擬似クラスです。フォームやカードの強調、ホバーだけでは開けないドロップダウンのキーボード対応に向いています。リングが出ないときは、:focus の指定順、* による一括リセット、そしてその要素がそもそもフォーカスできるかどうかを順に確認すれば、たいてい原因が見つかります。